All Skills

Use this skill when the user asks about "Shopify GraphQL", "Admin API", "metafields", "webhooks", "rate limiting", "pagination", "App Bridge", or any Shopify API integration work. Provides Shopify API patterns, rate limit handling, and best practices.

T
$npx skills add trantuananh-17/product-reviews --skill shopify-api-integration

Shopify API Best Practices

API Version Check (CRITICAL)

Always verify API version before implementing!

Shopify deprecates API versions regularly. Check:

  1. Current API version in shopify.app.toml or app config
  2. Shopify release notes for breaking changes
  3. Use Shopify MCP tools to verify current schema
// Check what version your app uses
// shopify.app.toml
[api]
api_version = "2024-10"  // Verify this matches your implementation

API Selection Guide

NeedSolution
Customize checkout UICheckout UI Extension
Apply discountsDiscount Function
Validate cartCart Validation Function
React to eventsWebhooks
Read/write dataGraphQL Admin API
Sync large dataBulk Operations
Store custom dataMetafields/Metaobjects

GraphQL Admin API

Basic Query

const query = `
  query getProduct($id: ID!) {
    product(id: $id) {
      id
      title
      handle
      variants(first: 10) {
        nodes {
          id
          price
        }
      }
    }
  }
`;

const response = await shopify.graphql(query, { id: productId });

Pagination

async function getAllProducts(shopify) {
  const products = [];
  let hasNextPage = true;
  let cursor = null;

  while (hasNextPage) {
    const query = `
      query getProducts($cursor: String) {
        products(first: 50, after: $cursor) {
          pageInfo { hasNextPage }
          edges {
            cursor
            node { id title }
          }
        }
      }
    `;

    const response = await shopify.graphql(query, { cursor });
    const { edges, pageInfo } = response.products;

    products.push(...edges.map(e => e.node));
    hasNextPage = pageInfo.hasNextPage;
    cursor = edges[edges.length - 1]?.cursor;
  }

  return products;
}

Bulk Operations (ALWAYS Consider First)

Before implementing any Shopify data sync, ask: "Can this hit API limits?"

Rate Limits Context:

  • Regular metafield API: 2 requests/second, 40 requests/minute
  • Bulk Operations: No rate limits - runs server-side on Shopify

Volume Decision Guide

VolumeStrategy
< 50 itemsRegular GraphQL
50-500 itemsBatch with Cloud Tasks + rate limiting
500+ itemsBulk Operations API

For detailed bulk mutation patterns, see: shopify-bulk-operations skill


Rate Limiting

// BAD: In-function sleep wastes CPU time
await sleep(60000); // 60s sleep = 60s CPU billed

// GOOD: Schedule retry with Cloud Tasks
async function scheduleRetry(payload, delaySeconds) {
  await client.createTask({
    parent: client.queuePath(project, location, 'shopify-retry'),
    task: {
      httpRequest: {
        url: `${baseUrl}/api/retry-shopify`,
        body: Buffer.from(JSON.stringify(payload)).toString('base64'),
        headers: { 'Content-Type': 'application/json' }
      },
      scheduleTime: {
        seconds: Math.floor(Date.now() / 1000) + delaySeconds
      }
    }
  });
}

Metafields

Set Metafields (Batch)

const mutation = `
  mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
    metafieldsSet(metafields: $metafields) {
      metafields { id key value }
      userErrors { field message }
    }
  }
`;

await shopify.graphql(mutation, {
  metafields: [
    {
      ownerId: customerId,
      namespace: 'loyalty',
      key: 'points',
      type: 'number_integer',
      value: '500'
    },
    {
      ownerId: customerId,
      namespace: 'loyalty',
      key: 'tier',
      type: 'single_line_text_field',
      value: 'Gold'
    }
  ]
});

Webhooks

Response Time (CRITICAL)

Must respond within 5 seconds!

// BAD: Heavy processing (may timeout)
app.post('/webhooks/orders/create', async (req, res) => {
  await calculatePoints(req.body);
  await updateCustomer(req.body);
  await syncToShopify(req.body);
  res.status(200).send('OK');
});

// GOOD: Queue and respond fast
app.post('/webhooks/orders/create', async (req, res) => {
  // Quick validation
  if (!verifyHmac(req)) {
    return res.status(401).send('Unauthorized');
  }

  // Queue for background processing
  await webhookQueueRef.add({
    type: 'orders/create',
    payload: req.body
  });

  // Respond immediately
  res.status(200).send('OK');
});

HMAC Verification

import crypto from 'crypto';

function verifyHmac(req) {
  const hmac = req.get('X-Shopify-Hmac-Sha256');
  const body = req.rawBody;
  const secret = process.env.SHOPIFY_WEBHOOK_SECRET;

  const hash = crypto
    .createHmac('sha256', secret)
    .update(body, 'utf8')
    .digest('base64');

  return crypto.timingSafeEqual(
    Buffer.from(hmac),
    Buffer.from(hash)
  );
}

App Bridge (Direct API)

When to Use

ScenarioUse App BridgeUse Firebase API
Simple Shopify CRUDYesNo
Need Firestore dataNoYes
Complex business logicNoYes
Background processingNoYes

Direct API Call

import { authenticatedFetch } from '@shopify/app-bridge/utilities';

async function fetchProducts(app) {
  const response = await authenticatedFetch(app)(
    '/admin/api/2024-04/graphql.json',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        query: `{ products(first: 10) { nodes { id title } } }`
      })
    }
  );

  return response.json();
}

Benefits:

  • Faster (no Firebase roundtrip)
  • Lower cost (no function invocation)
  • Uses shop's session directly