Products, Variants & Categories

Products, Variants & Categories

This guide explains how to model product variations and organize your catalog using BareCommerce's flexible product system.

Overview: The Flat Variant Model

BareCommerce uses a flat variant model instead of a traditional hierarchical structure:

  • No separate Variant table — Variants are just products with shared metadata
  • Grouped by variantGroupId — All variants of the same product share a group ID
  • Independent products — Each variant is a complete product record with its own price, SKU, stock, and attributes
  • Flexible attributes — Store custom attribute data (color, size, material, etc.) as JSON

This approach is powerful because:

  • Each variant can have different pricing and inventory
  • Variants can be managed independently via API
  • You can publish/unpublish individual variants
  • Simple queries return all information you need

Understanding Variants

What is a Variant?

A variant is a specific version of a product. For example, a "T-Shirt" product might have variants:

Product Title: "Classic T-Shirt"
├─ Variant 1: Red / Size M — SKU: TSHIRT-RED-M, Price: $19.99, Stock: 45
├─ Variant 2: Red / Size L — SKU: TSHIRT-RED-L, Price: $19.99, Stock: 32
├─ Variant 3: Blue / Size M — SKU: TSHIRT-BLUE-M, Price: $19.99, Stock: 0
└─ Variant 4: Blue / Size L — SKU: TSHIRT-BLUE-L, Price: $19.99, Stock: 28

In BareCommerce, each of these would be a separate Product record grouped under the same variantGroupId.

Variant Structure

Each variant has these key fields:

{
  // Shared variant group identity
  variantGroupId: "group_abc123",      // Ties all variants together
  
  // Variant-specific attributes
  attributes: {
    "color": "red",                     // JSON object of custom attributes
    "size": "M",
    "material": "cotton"
  },
  
  // Variant-specific properties
  sku: "TSHIRT-RED-M",                 // Unique per variant
  barcode: "123456789",                 // Optional
  
  // Variant-specific pricing
  price: "19.99",                       // String for precision
  compareAtPrice: "24.99",              // Original/sale price
  
  // Variant-specific inventory
  trackStock: true,
  stock: 45,
  allowBackorder: false,
  
  // Common product info (same for all variants)
  title: "Classic T-Shirt",
  description: "Comfortable everyday t-shirt",
  slug: "classic-t-shirt",
  categoryIds: ["cat_1", "cat_2"],      // All variants share categories
}

Creating Your First Variant Group

To create a product with variants, follow this workflow:

Step 1: Generate a Variant Group ID

const variantGroupId = `vg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Example: "vg_1699564800000_a1b2c3d4e"

Step 2: Create Each Variant

Create multiple product records with the same variantGroupId:

# Create Variant 1: Red / Size M
curl https://api.barecommercecore.com/api/stores/{storeId}/products \
  -H "X-API-Key: sk_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Classic T-Shirt",
    "slug": "classic-t-shirt",
    "description": "Comfortable everyday t-shirt",
    "price": "19.99",
    "compareAtPrice": "24.99",
    "sku": "TSHIRT-RED-M",
    "barcode": "123456789",
    "variantGroupId": "vg_1699564800000_a1b2c3d4e",
    "attributes": {
      "color": "red",
      "size": "M"
    },
    "trackStock": true,
    "stock": 45,
    "allowBackorder": false,
    "categoryIds": ["cat_shirts", "cat_red"],
    "primaryMediaId": "media_1"
  }'
 
# Create Variant 2: Red / Size L
curl https://api.barecommercecore.com/api/stores/{storeId}/products \
  -H "X-API-Key: sk_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Classic T-Shirt",
    "slug": "classic-t-shirt-l",
    "description": "Comfortable everyday t-shirt",
    "price": "19.99",
    "compareAtPrice": "24.99",
    "sku": "TSHIRT-RED-L",
    "barcode": "987654321",
    "variantGroupId": "vg_1699564800000_a1b2c3d4e",
    "attributes": {
      "color": "red",
      "size": "L"
    },
    "trackStock": true,
    "stock": 32,
    "allowBackorder": false,
    "categoryIds": ["cat_shirts", "cat_red"],
    "primaryMediaId": "media_1"
  }'

Note: The slug must be unique per store. If all variants share the same slug, BareCommerce will reject the second variant. Use a strategy like classic-t-shirt, classic-t-shirt-l, classic-t-shirt-xl to avoid collisions.

Step 3: Query All Variants Together

Fetch all variants of a product group:

curl "https://api.barecommercecore.com/api/stores/{storeId}/products/by-variant-group?variantGroupId=vg_1699564800000_a1b2c3d4e" \
  -H "X-API-Key: sk_live_xxxxx"

Response:

{
  "items": [
    {
      "id": "prod_1",
      "title": "Classic T-Shirt",
      "sku": "TSHIRT-RED-M",
      "attributes": { "color": "red", "size": "M" },
      "price": "19.99",
      "stock": 45,
      "variantGroupId": "vg_1699564800000_a1b2c3d4e"
    },
    {
      "id": "prod_2",
      "title": "Classic T-Shirt",
      "sku": "TSHIRT-RED-L",
      "attributes": { "color": "red", "size": "L" },
      "price": "19.99",
      "stock": 32,
      "variantGroupId": "vg_1699564800000_a1b2c3d4e"
    }
  ],
  "total": 2
}

Variant Attributes: Best Practices

Attributes are stored as a JSON object. Here's how to structure them:

// Good: Clear, machine-readable keys
attributes: {
  "color": "red",
  "size": "M",
  "material": "cotton",
  "fit": "regular"
}
 
// Bad: Inconsistent or unclear
attributes: {
  "Color": "red",           // Don't mix casing
  "sz": "M",                // Use full names
  "Material_Type": "cotton" // Don't use underscores in keys
}

Recommended attribute naming:

  • Use lowercase, snake_case keys
  • Keep values simple strings
  • Common attributes: color, size, material, weight, flavor, pattern

Variant Inventory Management

Each variant tracks its own inventory independently:

// Variant 1: In stock
{
  sku: "SHIRT-RED-M",
  trackStock: true,
  stock: 45,
  allowBackorder: false
}
 
// Variant 2: Low stock
{
  sku: "SHIRT-BLUE-M",
  trackStock: true,
  stock: 2,
  allowBackorder: false
}
 
// Variant 3: Out of stock but backordered
{
  sku: "SHIRT-GREEN-M",
  trackStock: true,
  stock: 0,
  allowBackorder: true
}
 
// Variant 4: Not tracked (infinite stock)
{
  sku: "DIGITAL-DOWNLOAD",
  trackStock: false,
  stock: 0,  // Ignored when trackStock=false
  allowBackorder: false
}

When a customer purchases a variant, you (or your inventory system) must decrement the stock field:

# Update stock after purchase
curl "https://api.barecommercecore.com/api/stores/{storeId}/products/{variantId}" \
  -X PATCH \
  -H "X-API-Key: sk_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "stock": 44  // Was 45, now 44 after 1 sale
  }'

Displaying Variants to Customers

When building a storefront, fetch all variants and group them by attribute:

// Fetch all variants
const response = await fetch(
  `https://api.barecommercecore.com/api/stores/${storeId}/products/by-variant-group?variantGroupId=${variantGroupId}`,
  { headers }
);
const { items } = await response.json();
 
// Group by color
const byColor = {};
items.forEach(variant => {
  const color = variant.attributes.color;
  if (!byColor[color]) byColor[color] = [];
  byColor[color].push(variant);
});
 
// Render
Object.entries(byColor).forEach(([color, variants]) => {
  console.log(`${color}: ${variants.map(v => v.attributes.size).join(', ')}`);
});
// Output:
// Red: M, L, XL
// Blue: M, L
// Green: M

Understanding Categories

What is a Category?

Categories organize products into a taxonomy. Examples:

Store
├─ Clothing
│  ├─ T-Shirts
│  ├─ Jeans
│  └─ Jackets
├─ Footwear
│  ├─ Sneakers
│  ├─ Boots
│  └─ Sandals
└─ Accessories
   ├─ Hats
   ├─ Belts
   └─ Scarves

Creating Categories

Categories support hierarchical structure with up to 2 levels (parent + children):

# Create parent category
curl https://api.barecommercecore.com/api/stores/{storeId}/categories \
  -X POST \
  -H "X-API-Key: sk_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Clothing",
    "slug": "clothing",
    "status": "published",
    "description": "All clothing items"
  }'
 
# Response
{
  "id": "cat_clothing",
  "name": "Clothing",
  "slug": "clothing",
  "status": "published",
  "parentId": null,
  "createdAt": "2024-01-15T10:30:00Z"
}
# Create child category
curl https://api.barecommercecore.com/api/stores/{storeId}/categories \
  -X POST \
  -H "X-API-Key: sk_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "T-Shirts",
    "slug": "t-shirts",
    "status": "published",
    "parentId": "cat_clothing",
    "description": "Casual and formal t-shirts"
  }'
 
# Response
{
  "id": "cat_tshirts",
  "name": "T-Shirts",
  "slug": "t-shirts",
  "status": "published",
  "parentId": "cat_clothing"
}

Category Limits

  • Maximum depth: 2 levels — One parent, one child level
  • No circular references — A category cannot be its own parent
  • Unique slugs — Each category slug must be unique per store

Assigning Products to Categories

Products belong to zero or more categories via the categoryIds array:

# Create product in multiple categories
curl https://api.barecommercecore.com/api/stores/{storeId}/products \
  -X POST \
  -H "X-API-Key: sk_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Classic T-Shirt",
    "slug": "classic-t-shirt",
    "price": "19.99",
    "categoryIds": ["cat_clothing", "cat_tshirts", "cat_sales"],
    "sku": "TSHIRT-001"
  }'

A product can be in multiple categories simultaneously. For example:

  • A red shirt could be in ClothingT-Shirts and also in Sale Items

Querying Products by Category

BareCommerce doesn't have a direct "get products in category" endpoint, but you can:

  1. Get all products and filter client-side:
const response = await fetch(
  `https://api.barecommercecore.com/api/stores/${storeId}/products`,
  { headers }
);
const { items } = await response.json();
 
// Filter to products in "cat_tshirts"
const categoryProducts = items.filter(product => 
  product.categoryIds.includes("cat_tshirts")
);
  1. Store custom indexes — Build your own mapping via webhooks or periodic sync

Category Hierarchy

Fetch the full category tree:

# Get all categories (flat list)
curl "https://api.barecommercecore.com/api/stores/{storeId}/categories" \
  -H "X-API-Key: sk_live_xxxxx"
 
# Get only root categories (no parent)
curl "https://api.barecommercecore.com/api/stores/{storeId}/categories?parentId=null" \
  -H "X-API-Key: sk_live_xxxxx"
 
# Get children of a specific category
curl "https://api.barecommercecore.com/api/stores/{storeId}/categories?parentId=cat_clothing" \
  -H "X-API-Key: sk_live_xxxxx"

Build a tree in your application:

const buildCategoryTree = (categories) => {
  const byId = Object.fromEntries(categories.map(c => [c.id, { ...c, children: [] }]));
  const root = [];
 
  categories.forEach(category => {
    if (category.parentId) {
      byId[category.parentId].children.push(byId[category.id]);
    } else {
      root.push(byId[category.id]);
    }
  });
 
  return root;
};

Common Patterns

Pattern 1: Clothing Store with Size & Color Variants

// Create size/color attributes
const colors = ["Black", "White", "Navy"];
const sizes = ["XS", "S", "M", "L", "XL", "XXL"];
 
// Generate variant group
const variantGroupId = `shirt_${Date.now()}`;
 
// Create variant for each combination
for (const color of colors) {
  for (const size of sizes) {
    const sku = `SHIRT-${color.toUpperCase()}-${size}`;
    const stock = Math.floor(Math.random() * 100); // Random for demo
    
    await createProduct({
      variantGroupId,
      attributes: { color, size },
      sku,
      stock,
      // ... other fields
    });
  }
}

Pattern 2: Digital Products (Infinite Stock)

await createProduct({
  title: "PDF Guide",
  description: "Instant download guide",
  price: "9.99",
  sku: "PDF-GUIDE-001",
  trackStock: false,  // No inventory tracking
  stock: 0,           // Ignored
  variantGroupId: null,  // Single product, no variants
});

Pattern 3: Seasonal Sales Category

# Create a "Summer Sale" category
curl https://api.barecommercecore.com/api/stores/{storeId}/categories \
  -X POST \
  -H "X-API-Key: sk_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer Sale",
    "slug": "summer-sale",
    "status": "published"
  }'
 
# Assign multiple products to it
# (Update each product with categoryIds including the sale category)

Pattern 4: Product Collections (Multiple Categories)

// One product in multiple independent categories
const productIds = ["prod_1", "prod_2", "prod_3"];
 
productIds.forEach(productId => {
  updateProduct(productId, {
    categoryIds: [
      "cat_featured",      // Featured products
      "cat_bestsellers",   // Best sellers
      "cat_summer_2024"    // Seasonal
    ]
  });
});

API Reference

Variant Queries

# List products with specific SKU
GET /api/stores/{storeId}/products?sku=SHIRT-RED-M
 
# List all variants in a group
GET /api/stores/{storeId}/products/by-variant-group?variantGroupId=vg_xxx
 
# Filter by attribute (custom implementation)
GET /api/stores/{storeId}/products/by-attributes?color=red&size=M

Category Queries

# List all categories
GET /api/stores/{storeId}/categories
 
# Get root categories only
GET /api/stores/{storeId}/categories?parentId=null
 
# Get children of a category
GET /api/stores/{storeId}/categories?parentId=cat_clothing
 
# Search categories
GET /api/stores/{storeId}/categories?search=shirt

Best Practices

Variant Naming

  • Use a consistent SKU format: PRODUCT-VARIANT-ID
  • Make SKUs human-readable (e.g., SHIRT-RED-M not SKU12345)
  • Avoid special characters in SKUs

Category Organization

  • Limit to 2 levels for simplicity
  • Use singular nouns in category names ("T-Shirt", not "T-Shirts")
  • Keep slug names lowercase and dash-separated
  • Don't create categories you won't use

Inventory Accuracy

  • Only set trackStock: true if you actually manage inventory
  • Update stock immediately after orders (via webhook listener)
  • Use allowBackorder: true only for items you can restock
  • Sync with your warehouse system regularly

SEO Optimization

  • Fill seoTitle and seoDescription for all variants
  • Use category seoTitle for better search rankings
  • Include relevant keywords in descriptions

Troubleshooting

Variant Not Appearing

Problem: Created a variant but it's not showing up in listings.

Solution:

  • Check the variantGroupId — make sure all variants have the same ID
  • Verify the product status is "published", not "draft"
  • Confirm you're querying the correct storeId

Slug Uniqueness Error

Problem: Getting error Unique constraint failed: products.storeId, products.slug

Solution:

  • Variant slugs must be unique across all variants
  • Use suffixes like product-name, product-name-l, product-name-xl
  • Or include the full variant combo: shirt-red-medium

Category Not Showing Products

Problem: Created a category and assigned products, but products don't appear.

Solution:

  • Categories are independent — filtering happens via categoryIds array on products
  • Build your own "products in category" view using the product categoryIds field
  • Ensure product status is "published"

Next Steps