From shopify-pack
Optimizes Shopify GraphQL API performance through query cost reduction, bulk operations, caching strategies, and Storefront API for high-traffic storefronts.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin shopify-packThis skill is limited to using the following tools:
Optimize Shopify API performance through GraphQL query cost reduction, bulk operations for large data exports, response caching, and Storefront API for high-traffic public-facing queries.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Optimize Shopify API performance through GraphQL query cost reduction, bulk operations for large data exports, response caching, and Storefront API for high-traffic public-facing queries.
Shopify-GraphQL-Cost-Debug: 1 header for cost analysis# Debug query cost with special header
curl -X POST "https://$STORE/admin/api/2024-10/graphql.json" \
-H "X-Shopify-Access-Token: $TOKEN" \
-H "Content-Type: application/json" \
-H "Shopify-GraphQL-Cost-Debug: 1" \
-d '{"query": "{ products(first: 50) { edges { node { id title variants(first: 20) { edges { node { id price } } } } } } }"}' \
| jq '.extensions.cost'
Response shows cost breakdown:
{
"requestedQueryCost": 152,
"actualQueryCost": 42,
"throttleStatus": {
"maximumAvailable": 1000.0,
"currentlyAvailable": 958.0,
"restoreRate": 50.0
}
}
Key rule: requestedQueryCost is calculated as first * nested_fields. Reducing first: from 250 to 50 can cut cost by 5x.
// BEFORE: High cost — requests too many fields and items
// requestedQueryCost: ~5,502
const EXPENSIVE_QUERY = `{
products(first: 250) {
edges {
node {
id title description descriptionHtml vendor productType tags
variants(first: 100) {
edges {
node {
id title price compareAtPrice sku barcode
inventoryQuantity weight weightUnit
selectedOptions { name value }
metafields(first: 10) {
edges { node { namespace key value type } }
}
}
}
}
images(first: 20) {
edges { node { url altText width height } }
}
metafields(first: 10) {
edges { node { namespace key value type } }
}
}
}
}
}`;
// AFTER: Optimized — only needed fields, smaller page sizes
// requestedQueryCost: ~112
const OPTIMIZED_QUERY = `{
products(first: 50) {
edges {
node {
id
title
status
variants(first: 5) {
edges {
node { id price sku inventoryQuantity }
}
}
}
}
pageInfo { hasNextPage endCursor }
}
}`;
Bulk operations bypass rate limits and are designed for exporting large datasets:
// Step 1: Start bulk operation
const START_BULK = `
mutation {
bulkOperationRunQuery(query: """
{
products {
edges {
node {
id
title
handle
variants {
edges {
node {
id
sku
price
inventoryQuantity
}
}
}
}
}
}
}
""") {
bulkOperation {
id
status
}
userErrors { field message }
}
}
`;
// Step 2: Poll for completion
const CHECK_BULK = `{
currentBulkOperation {
id
status # CREATED, RUNNING, COMPLETED, FAILED
errorCode
objectCount
fileSize
url # JSONL download URL when COMPLETED
createdAt
}
}`;
// Step 3: Download results (JSONL format — one JSON object per line)
// const response = await fetch(bulkOperation.url);
// Each line: {"id":"gid://shopify/Product/123","title":"Widget",...}
import { LRUCache } from "lru-cache";
const shopifyCache = new LRUCache<string, any>({
max: 500,
ttl: 5 * 60 * 1000, // 5 minutes
updateAgeOnGet: true,
});
async function cachedQuery<T>(
cacheKey: string,
queryFn: () => Promise<T>,
ttlMs?: number
): Promise<T> {
const cached = shopifyCache.get(cacheKey);
if (cached !== undefined) return cached as T;
const result = await queryFn();
shopifyCache.set(cacheKey, result, { ttl: ttlMs });
return result;
}
// Usage — cache product data for 5 minutes
const product = await cachedQuery(
`product:${productId}`,
() => shopifyQuery(shop, PRODUCT_QUERY, { id: productId })
);
// Invalidate on webhook
app.post("/webhooks", (req, res) => {
const topic = req.headers["x-shopify-topic"];
if (topic === "products/update") {
const payload = JSON.parse(req.body);
shopifyCache.delete(`product:gid://shopify/Product/${payload.id}`);
}
});
The Storefront API has separate rate limits and is designed for high-traffic public storefronts:
// Storefront API — safe for client-side, higher rate limits
const storefrontClient = new shopify.clients.Storefront({
session,
apiVersion: "2024-10",
});
// Storefront API query — no admin credentials exposed
const products = await storefrontClient.request(`{
products(first: 12, sortKey: BEST_SELLING) {
edges {
node {
id
title
handle
priceRange {
minVariantPrice { amount currencyCode }
}
featuredImage {
url(transform: { maxWidth: 400 })
altText
}
}
}
}
}`);
| Issue | Cause | Solution |
|---|---|---|
THROTTLED on every query | requestedQueryCost too high | Reduce first: and remove unused fields |
| Bulk operation FAILED | Query syntax error | Test query in GraphiQL first |
| Stale cache data | Cache not invalidated | Add webhook handlers to clear cache |
| Storefront API 403 | Wrong token type | Use Storefront API access token, not Admin |
| Approach | 10K Products Export | Rate Limit Impact |
|---|---|---|
| Paginated (first: 250) | 40 queries, ~60s | Uses ~6,000 points |
| Paginated (first: 50) | 200 queries, ~300s | Uses ~22,000 points |
| Bulk Operation | 1 query + poll, ~30s | Minimal impact |
For cost optimization, see shopify-cost-tuning.