How to properly update product variant metafields with Shopify REST API (sub-collections use case)?

I’m trying to programmatically sync the sub-collections of a Shopify product as a metafield on each of its variants using the Shopify REST Admin API and Node.js (Axios).

Here’s what I’m doing:

  1. Fetch all variants for a given product
  2. Fetch custom_collections and smart_collections this product belongs to
  3. Build a comma-separated list of collection titles
  4. For each variant:
  • Check if a metafield with a given namespace/key exists
  • If it exists, update it
  • If not, create it

Here’s a condensed version of my script:

const axios = require('axios');

const SHOPIFY_API_URL        = process.env.SHOPIFY_ADMIN_API_URL.replace(/\/+$/, '');
const SHOPIFY_TOKEN          = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN;
const METAFIELD_NAMESPACE    = process.env.SHOPIFY_METAFIELD_NAMESPACE || 'custom';
const METAFIELD_KEY          = process.env.SHOPIFY_METAFIELD_KEY       || 'sub_collections';
const METAFIELD_TYPE         = 'single_line_text_field';

async function syncSubCollections(productId) {
  if (!productId) throw new Error('syncSubCollections: missing productId');

  // 1) fetch product variants
  const { data: prod } = await axios.get(
    `${SHOPIFY_API_URL}/products/${productId}.json?fields=variants`,
    { headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN } }
  );
  const variantIds = prod.product.variants.map(v => v.id);

  // 2) fetch the two REST endpoints for collections associated with the product
  const [customRes, smartRes] = await Promise.all([
    axios.get(
      `${SHOPIFY_API_URL}/custom_collections.json?product_id=${productId}`,
      { headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN } }
    ),
    axios.get(
      `${SHOPIFY_API_URL}/smart_collections.json?product_id=${productId}`,
      { headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN } }
    ),
  ]);

  // 3) build a comma-separated list of collection titles
  const collections = [
    ...customRes.data.custom_collections.map(c => c.title),
    ...smartRes.data.smart_collections.map(c => c.title),
  ];
  const value = collections.join(', ');

  // 4) upsert the metafield on each variant
  await Promise.all(variantIds.map(async variantId => {
    // list existing metafields
    const { data: mfList } = await axios.get(
      `${SHOPIFY_API_URL}/variants/${variantId}/metafields.json`,
      { headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN } }
    );
    const existing = mfList.metafields.find(mf =>
      mf.namespace === METAFIELD_NAMESPACE && mf.key === METAFIELD_KEY
    );

    if (existing) {
      // update
      await axios.put(
        `${SHOPIFY_API_URL}/metafields/${existing.id}.json`,
        { metafield: { id: existing.id, value, type: METAFIELD_TYPE } },
        { headers: {
            'X-Shopify-Access-Token': SHOPIFY_TOKEN,
            'Content-Type': 'application/json'
          }
        }
      );
    } else {
      // create
      await axios.post(
        `${SHOPIFY_API_URL}/metafields.json`,
        { metafield: {
            namespace:     METAFIELD_NAMESPACE,
            key:           METAFIELD_KEY,
            owner_resource:'variant',
            owner_id:      variantId,
            type:          METAFIELD_TYPE,
            value
          }
        },
        { headers: {
            'X-Shopify-Access-Token': SHOPIFY_TOKEN,
            'Content-Type': 'application/json'
          }
        }
      );
    }
  }));

  // return what we synced
  return { productId, collections };
}

module.exports = { syncSubCollections };

The metafields don’t get updated properly — sometimes they don’t appear at all or the value does not persist. No clear error is returned.

Questions:

  • Is this the correct way to upsert metafields for each variant?
  • Am I using the right REST endpoints for updating vs creating variant metafields?
  • Is there a better way (GraphQL or bulk mutation) to do this efficiently for multiple variants?

I would recommend upgrading to GraphQL as products via REST is deprecated .

You can also likely simplify your logic as you can get the list of collections a product is in, in one go Product - GraphQL Admin

You can then use either metafieldSet or productSet to update the data.
Metafields set will let you update multiple metafields at once.

2 Likes