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:
- Fetch all variants for a given product
- Fetch custom_collections and smart_collections this product belongs to
- Build a comma-separated list of collection titles
- 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?