GraphQL to Fetch Data Efficiently from the Shopify API

How to Use GraphQL to Fetch Data Efficiently from the Shopify API

The Problem

Shopify stores contain thousands of products, orders, and customer records. Traditional REST APIs force you to make dozens of sequential requests to gather related data—first fetch products, then variants, then inventory levels, then metafields, each requiring separate HTTP calls that waste bandwidth and slow your application. A single dashboard showing product details with inventory across locations requires 5+ REST endpoints, taking seconds to load. GraphQL solves this by letting you request exactly the data you need in a single query. However, Shopify’s GraphQL API has unique challenges: query cost calculations that throttle requests, cursor-based pagination requiring careful iteration, connection patterns for related data, bulk operations for large datasets, and webhook integration for real-time updates. You need to understand GraphQL query syntax, handle rate limits using query cost analysis, implement pagination correctly, transform nested response structures, and avoid common pitfalls like requesting too much data or hitting the query complexity limit. This tutorial provides production-ready code for efficiently fetching Shopify data using GraphQL with proper error handling and rate limit management.

Tech Stack & Prerequisites

  • Node.js v18+ with npm
  • Shopify Store (development store or production)
  • Shopify Admin API Access Token with appropriate scopes
  • @shopify/shopify-api 8.0+ (official Shopify library)
  • graphql-request 6.1+ (lightweight GraphQL client)
  • dotenv for environment variables
  • express 4.18+ for API server
  • axios 1.6+ for HTTP requests (backup)
  • date-fns 3.0+ for date handling

Required Shopify Setup:

  • Shopify Partner account (for development stores)
  • Custom app created in Shopify admin
  • Admin API access token generated
  • API scopes configured (read_products, read_orders, etc.)
  • GraphQL Admin API access enabled

Recommended:

  • GraphQL knowledge (query syntax, variables)
  • Shopify GraphiQL app for testing queries
  • Understanding of pagination patterns

Step-by-Step Implementation

Step 1: Setup

Initialize the project:

bash
mkdir shopify-graphql-integration
cd shopify-graphql-integration
npm init -y
npm install @shopify/shopify-api graphql-request dotenv express axios date-fns
npm install --save-dev nodemon

Create project structure:

bash
mkdir src queries utils
touch src/server.js queries/products.js queries/orders.js queries/customers.js
touch utils/shopifyClient.js utils/rateLimiter.js utils/pagination.js
touch .env .gitignore
```

Your structure should be:
```
shopify-graphql-integration/
├── src/
│   └── server.js
├── queries/
│   ├── products.js
│   ├── orders.js
│   └── customers.js
├── utils/
│   ├── shopifyClient.js
│   ├── rateLimiter.js
│   └── pagination.js
├── .env
├── .gitignore
└── package.json

package.json — Add scripts:

json
{
  "name": "shopify-graphql-integration",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js",
    "test": "node --experimental-modules src/test.js"
  },
  "dependencies": {
    "@shopify/shopify-api": "^8.0.0",
    "axios": "^1.6.2",
    "date-fns": "^3.0.0",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "graphql-request": "^6.1.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

Update .gitignore:

bash
echo "node_modules/
.env
*.log" > .gitignore

Step 2: Configuration

.env — Store Shopify credentials securely:

env
# Shopify Store Configuration
SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
SHOPIFY_ACCESS_TOKEN=shpat_your_admin_api_access_token_here
SHOPIFY_API_VERSION=2024-01

# Server Configuration
PORT=3000
NODE_ENV=development

# Rate Limiting
RATE_LIMIT_MAX_COST=1000
RATE_LIMIT_RESTORE_RATE=50

How to get Shopify Admin API Access Token:

  1. Create Custom App:
    • Log into Shopify Admin
    • Go to SettingsApps and sales channels
    • Click Develop appsCreate an app
    • Name: “GraphQL Data Integration”
  2. Configure API Scopes:
    • Click Configure Admin API scopes
    • Select required scopes:
      • read_products
      • read_orders
      • read_customers
      • read_inventory
      • read_fulfillments
    • Save
  3. Install App and Get Token:
    • Click Install app
    • Reveal Admin API access token
    • Copy token (starts with shpat_)
    • Store in .env

utils/shopifyClient.js — GraphQL client with rate limit handling:

javascript
import { GraphQLClient } from 'graphql-request';
import dotenv from 'dotenv';

dotenv.config();

const SHOPIFY_STORE_DOMAIN = process.env.SHOPIFY_STORE_DOMAIN;
const SHOPIFY_ACCESS_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN;
const API_VERSION = process.env.SHOPIFY_API_VERSION || '2024-01';

// Create GraphQL client
const endpoint = `https://${SHOPIFY_STORE_DOMAIN}/admin/api/${API_VERSION}/graphql.json`;

export const shopifyClient = new GraphQLClient(endpoint, {
  headers: {
    'X-Shopify-Access-Token': SHOPIFY_ACCESS_TOKEN,
    'Content-Type': 'application/json',
  },
});

// Execute GraphQL query with error handling
export async function executeQuery(query, variables = {}) {
  try {
    const response = await shopifyClient.request(query, variables);
    
    // Check for GraphQL errors
    if (response.errors) {
      console.error('GraphQL errors:', response.errors);
      throw new Error(response.errors[0].message);
    }

    // Extract cost information from extensions
    const cost = response.extensions?.cost;
    if (cost) {
      console.log(`Query cost: ${cost.actualQueryCost}/${cost.throttleStatus.currentlyAvailable}`);
      
      // Warn if approaching limit
      if (cost.actualQueryCost > cost.throttleStatus.currentlyAvailable * 0.8) {
        console.warn('⚠️  Approaching rate limit, consider slowing requests');
      }
    }

    return response;
  } catch (error) {
    console.error('GraphQL request failed:', error.message);
    
    // Handle rate limiting
    if (error.response?.status === 429) {
      console.error('Rate limit exceeded. Retry after delay.');
      throw new Error('RATE_LIMIT_EXCEEDED');
    }

    throw error;
  }
}

// Get query cost estimate without executing
export async function estimateQueryCost(query, variables = {}) {
  const costQuery = `
    query {
      cost: __typename @cost
    }
  `;

  // Note: Shopify doesn't provide cost estimation endpoint
  // This is a placeholder - actual cost is returned with query execution
  return null;
}

export default shopifyClient;

utils/rateLimiter.js — Rate limit management:

javascript
import dotenv from 'dotenv';

dotenv.config();

class RateLimiter {
  constructor() {
    this.maxCost = parseInt(process.env.RATE_LIMIT_MAX_COST) || 1000;
    this.restoreRate = parseInt(process.env.RATE_LIMIT_RESTORE_RATE) || 50;
    this.currentlyAvailable = this.maxCost;
    this.lastRestoreTime = Date.now();
  }

  // Update available points based on query response
  updateFromResponse(costData) {
    if (costData && costData.throttleStatus) {
      this.currentlyAvailable = costData.throttleStatus.currentlyAvailable;
      this.maxCost = costData.throttleStatus.maximumAvailable;
      this.restoreRate = costData.throttleStatus.restoreRate;
    }
  }

  // Calculate wait time if needed
  async waitIfNeeded(estimatedCost) {
    // Restore points based on time elapsed
    const now = Date.now();
    const elapsed = (now - this.lastRestoreTime) / 1000; // seconds
    const pointsRestored = Math.floor(elapsed * this.restoreRate);
    
    this.currentlyAvailable = Math.min(
      this.maxCost,
      this.currentlyAvailable + pointsRestored
    );
    this.lastRestoreTime = now;

    // Check if we need to wait
    if (this.currentlyAvailable < estimatedCost) {
      const pointsNeeded = estimatedCost - this.currentlyAvailable;
      const waitTime = (pointsNeeded / this.restoreRate) * 1000; // milliseconds

      console.log(`⏳ Waiting ${(waitTime / 1000).toFixed(1)}s for rate limit...`);
      await new Promise(resolve => setTimeout(resolve, waitTime));

      // Update after wait
      this.currentlyAvailable += pointsNeeded;
    }

    return true;
  }

  // Deduct cost after query execution
  deductCost(actualCost) {
    this.currentlyAvailable = Math.max(0, this.currentlyAvailable - actualCost);
  }

  // Get current status
  getStatus() {
    return {
      available: this.currentlyAvailable,
      maximum: this.maxCost,
      restoreRate: this.restoreRate,
    };
  }
}

export const rateLimiter = new RateLimiter();
export default rateLimiter;

utils/pagination.js — Cursor-based pagination helper:

javascript
// Fetch all pages of data using cursor pagination
export async function fetchAllPages(executeQueryFn, query, variables, dataPath) {
  const allItems = [];
  let hasNextPage = true;
  let cursor = null;

  while (hasNextPage) {
    // Add cursor to variables
    const queryVariables = {
      ...variables,
      after: cursor,
    };

    // Execute query
    const response = await executeQueryFn(query, queryVariables);

    // Navigate to data using path (e.g., 'products.edges')
    const data = getNestedProperty(response, dataPath);

    if (!data || !data.edges || data.edges.length === 0) {
      break;
    }

    // Extract nodes from edges
    const items = data.edges.map(edge => edge.node);
    allItems.push(...items);

    // Check for next page
    hasNextPage = data.pageInfo?.hasNextPage || false;
    cursor = data.pageInfo?.endCursor || null;

    console.log(`Fetched ${items.length} items (total: ${allItems.length})`);

    if (!hasNextPage) {
      console.log('✓ All pages fetched');
    }
  }

  return allItems;
}

// Helper to get nested property from object
function getNestedProperty(obj, path) {
  return path.split('.').reduce((current, key) => current?.[key], obj);
}

// Build connection query with pagination fields
export function buildConnectionQuery(fields, connectionName) {
  return `
    ${connectionName}(first: $first, after: $after) {
      edges {
        node {
          ${fields}
        }
        cursor
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  `;
}

export default { fetchAllPages, buildConnectionQuery };

Step 3: Core Logic

queries/products.js — Product queries:

javascript
import { executeQuery } from '../utils/shopifyClient.js';
import { fetchAllPages } from '../utils/pagination.js';

// Fetch all products with variants and inventory
export const GET_PRODUCTS_QUERY = `
  query GetProducts($first: Int!, $after: String) {
    products(first: $first, after: $after) {
      edges {
        node {
          id
          title
          handle
          status
          createdAt
          updatedAt
          productType
          vendor
          tags
          totalInventory
          variants(first: 10) {
            edges {
              node {
                id
                title
                sku
                price
                compareAtPrice
                inventoryQuantity
                inventoryItem {
                  id
                  tracked
                }
              }
            }
          }
          images(first: 5) {
            edges {
              node {
                id
                url
                altText
              }
            }
          }
          metafields(first: 10) {
            edges {
              node {
                key
                value
                namespace
              }
            }
          }
        }
        cursor
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

// Fetch single product by ID
export const GET_PRODUCT_BY_ID_QUERY = `
  query GetProduct($id: ID!) {
    product(id: $id) {
      id
      title
      handle
      description
      descriptionHtml
      status
      createdAt
      updatedAt
      productType
      vendor
      tags
      totalInventory
      variants(first: 100) {
        edges {
          node {
            id
            title
            sku
            price
            compareAtPrice
            inventoryQuantity
            weight
            weightUnit
            inventoryItem {
              id
              tracked
              inventoryLevels(first: 10) {
                edges {
                  node {
                    id
                    available
                    location {
                      name
                    }
                  }
                }
              }
            }
          }
        }
      }
      images(first: 10) {
        edges {
          node {
            id
            url
            altText
            width
            height
          }
        }
      }
    }
  }
`;

// Fetch all products (with pagination)
export async function getAllProducts(limit = 50) {
  return fetchAllPages(
    executeQuery,
    GET_PRODUCTS_QUERY,
    { first: limit },
    'products'
  );
}

// Fetch single product by ID
export async function getProductById(productId) {
  const response = await executeQuery(GET_PRODUCT_BY_ID_QUERY, { id: productId });
  return response.product;
}

// Search products by query
export const SEARCH_PRODUCTS_QUERY = `
  query SearchProducts($query: String!, $first: Int!) {
    products(first: $first, query: $query) {
      edges {
        node {
          id
          title
          handle
          productType
          vendor
          totalInventory
        }
      }
    }
  }
`;

export async function searchProducts(searchQuery, limit = 20) {
  const response = await executeQuery(SEARCH_PRODUCTS_QUERY, {
    query: searchQuery,
    first: limit,
  });

  return response.products.edges.map(edge => edge.node);
}

export default {
  getAllProducts,
  getProductById,
  searchProducts,
};

queries/orders.js — Order queries:

javascript
import { executeQuery } from '../utils/shopifyClient.js';
import { fetchAllPages } from '../utils/pagination.js';

// Fetch orders with line items and fulfillments
export const GET_ORDERS_QUERY = `
  query GetOrders($first: Int!, $after: String) {
    orders(first: $first, after: $after, sortKey: CREATED_AT, reverse: true) {
      edges {
        node {
          id
          name
          email
          createdAt
          updatedAt
          totalPriceSet {
            shopMoney {
              amount
              currencyCode
            }
          }
          subtotalPriceSet {
            shopMoney {
              amount
              currencyCode
            }
          }
          totalTaxSet {
            shopMoney {
              amount
              currencyCode
            }
          }
          displayFulfillmentStatus
          displayFinancialStatus
          customer {
            id
            firstName
            lastName
            email
          }
          lineItems(first: 50) {
            edges {
              node {
                id
                title
                quantity
                variant {
                  id
                  sku
                  title
                }
                originalTotalSet {
                  shopMoney {
                    amount
                    currencyCode
                  }
                }
              }
            }
          }
          fulfillments(first: 10) {
            trackingInfo {
              number
              url
            }
            status
            createdAt
          }
          shippingAddress {
            address1
            address2
            city
            province
            country
            zip
          }
        }
        cursor
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

// Get order by ID
export const GET_ORDER_BY_ID_QUERY = `
  query GetOrder($id: ID!) {
    order(id: $id) {
      id
      name
      email
      createdAt
      cancelledAt
      closedAt
      displayFulfillmentStatus
      displayFinancialStatus
      totalPriceSet {
        shopMoney {
          amount
          currencyCode
        }
      }
      customer {
        id
        firstName
        lastName
        email
        phone
      }
      lineItems(first: 100) {
        edges {
          node {
            id
            title
            quantity
            variant {
              id
              sku
              title
              product {
                id
                title
              }
            }
            discountedTotalSet {
              shopMoney {
                amount
                currencyCode
              }
            }
          }
        }
      }
      fulfillments(first: 10) {
        id
        status
        trackingInfo {
          number
          url
          company
        }
        createdAt
      }
      transactions(first: 10) {
        id
        status
        kind
        amountSet {
          shopMoney {
            amount
            currencyCode
          }
        }
      }
    }
  }
`;

// Fetch all orders
export async function getAllOrders(limit = 50) {
  return fetchAllPages(
    executeQuery,
    GET_ORDERS_QUERY,
    { first: limit },
    'orders'
  );
}

// Fetch order by ID
export async function getOrderById(orderId) {
  const response = await executeQuery(GET_ORDER_BY_ID_QUERY, { id: orderId });
  return response.order;
}

// Get orders by date range
export const GET_ORDERS_BY_DATE_QUERY = `
  query GetOrdersByDate($first: Int!, $after: String, $query: String!) {
    orders(first: $first, after: $after, query: $query, sortKey: CREATED_AT, reverse: true) {
      edges {
        node {
          id
          name
          createdAt
          totalPriceSet {
            shopMoney {
              amount
              currencyCode
            }
          }
          displayFulfillmentStatus
          displayFinancialStatus
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

export async function getOrdersByDateRange(startDate, endDate) {
  const query = `created_at:>='${startDate}' AND created_at:<='${endDate}'`;
  
  return fetchAllPages(
    executeQuery,
    GET_ORDERS_BY_DATE_QUERY,
    { first: 50, query },
    'orders'
  );
}

export default {
  getAllOrders,
  getOrderById,
  getOrdersByDateRange,
};

queries/customers.js — Customer queries:

javascript
import { executeQuery } from '../utils/shopifyClient.js';
import { fetchAllPages } from '../utils/pagination.js';

// Get customers with order statistics
export const GET_CUSTOMERS_QUERY = `
  query GetCustomers($first: Int!, $after: String) {
    customers(first: $first, after: $after, sortKey: CREATED_AT, reverse: true) {
      edges {
        node {
          id
          firstName
          lastName
          email
          phone
          createdAt
          updatedAt
          state
          note
          tags
          ordersCount
          totalSpentV2 {
            amount
            currencyCode
          }
          addresses(first: 5) {
            address1
            address2
            city
            province
            country
            zip
          }
          defaultAddress {
            address1
            city
            province
            country
            zip
          }
        }
        cursor
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

// Get customer by ID with full details
export const GET_CUSTOMER_BY_ID_QUERY = `
  query GetCustomer($id: ID!) {
    customer(id: $id) {
      id
      firstName
      lastName
      email
      phone
      createdAt
      updatedAt
      state
      note
      tags
      ordersCount
      totalSpentV2 {
        amount
        currencyCode
      }
      addresses(first: 10) {
        id
        address1
        address2
        city
        province
        country
        zip
        phone
      }
      orders(first: 10, sortKey: CREATED_AT, reverse: true) {
        edges {
          node {
            id
            name
            createdAt
            totalPriceSet {
              shopMoney {
                amount
                currencyCode
              }
            }
            displayFulfillmentStatus
          }
        }
      }
      metafields(first: 20) {
        edges {
          node {
            id
            key
            value
            namespace
          }
        }
      }
    }
  }
`;

// Fetch all customers
export async function getAllCustomers(limit = 50) {
  return fetchAllPages(
    executeQuery,
    GET_CUSTOMERS_QUERY,
    { first: limit },
    'customers'
  );
}

// Fetch customer by ID
export async function getCustomerById(customerId) {
  const response = await executeQuery(GET_CUSTOMER_BY_ID_QUERY, { id: customerId });
  return response.customer;
}

// Search customers by email
export const SEARCH_CUSTOMERS_QUERY = `
  query SearchCustomers($query: String!, $first: Int!) {
    customers(first: $first, query: $query) {
      edges {
        node {
          id
          firstName
          lastName
          email
          ordersCount
          totalSpentV2 {
            amount
            currencyCode
          }
        }
      }
    }
  }
`;

export async function searchCustomersByEmail(email) {
  const query = `email:${email}`;
  const response = await executeQuery(SEARCH_CUSTOMERS_QUERY, {
    query,
    first: 10,
  });

  return response.customers.edges.map(edge => edge.node);
}

export default {
  getAllCustomers,
  getCustomerById,
  searchCustomersByEmail,
};

src/server.js — Express API server:

javascript
import express from 'express';
import dotenv from 'dotenv';
import { getAllProducts, getProductById, searchProducts } from '../queries/products.js';
import { getAllOrders, getOrderById, getOrdersByDateRange } from '../queries/orders.js';
import { getAllCustomers, getCustomerById, searchCustomersByEmail } from '../queries/customers.js';
import { rateLimiter } from '../utils/rateLimiter.js';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

// Health check
app.get('/health', (req, res) => {
  res.json({
    status: 'healthy',
    rateLimitStatus: rateLimiter.getStatus(),
  });
});

// GET /api/products - Fetch all products
app.get('/api/products', async (req, res) => {
  try {
    const { limit = 50 } = req.query;

    console.log(`\n📦 Fetching products (limit: ${limit})...`);

    const products = await getAllProducts(parseInt(limit));

    res.json({
      success: true,
      count: products.length,
      data: products,
    });
  } catch (error) {
    console.error('Error fetching products:', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// GET /api/products/:id - Fetch single product
app.get('/api/products/:id', async (req, res) => {
  try {
    const productId = `gid://shopify/Product/${req.params.id}`;

    console.log(`\n📦 Fetching product: ${productId}`);

    const product = await getProductById(productId);

    if (!product) {
      return res.status(404).json({
        success: false,
        error: 'Product not found',
      });
    }

    res.json({
      success: true,
      data: product,
    });
  } catch (error) {
    console.error('Error fetching product:', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// GET /api/products/search - Search products
app.get('/api/products/search', async (req, res) => {
  try {
    const { q, limit = 20 } = req.query;

    if (!q) {
      return res.status(400).json({
        success: false,
        error: 'Query parameter "q" required',
      });
    }

    console.log(`\n🔍 Searching products: "${q}"`);

    const products = await searchProducts(q, parseInt(limit));

    res.json({
      success: true,
      count: products.length,
      data: products,
    });
  } catch (error) {
    console.error('Error searching products:', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// GET /api/orders - Fetch all orders
app.get('/api/orders', async (req, res) => {
  try {
    const { limit = 50, startDate, endDate } = req.query;

    console.log(`\n📋 Fetching orders...`);

    let orders;

    if (startDate && endDate) {
      orders = await getOrdersByDateRange(startDate, endDate);
    } else {
      orders = await getAllOrders(parseInt(limit));
    }

    res.json({
      success: true,
      count: orders.length,
      data: orders,
    });
  } catch (error) {
    console.error('Error fetching orders:', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// GET /api/orders/:id - Fetch single order
app.get('/api/orders/:id', async (req, res) => {
  try {
    const orderId = `gid://shopify/Order/${req.params.id}`;

    console.log(`\n📋 Fetching order: ${orderId}`);

    const order = await getOrderById(orderId);

    if (!order) {
      return res.status(404).json({
        success: false,
        error: 'Order not found',
      });
    }

    res.json({
      success: true,
      data: order,
    });
  } catch (error) {
    console.error('Error fetching order:', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// GET /api/customers - Fetch all customers
app.get('/api/customers', async (req, res) => {
  try {
    const { limit = 50 } = req.query;

    console.log(`\n👥 Fetching customers (limit: ${limit})...`);

    const customers = await getAllCustomers(parseInt(limit));

    res.json({
      success: true,
      count: customers.length,
      data: customers,
    });
  } catch (error) {
    console.error('Error fetching customers:', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// GET /api/customers/:id - Fetch single customer
app.get('/api/customers/:id', async (req, res) => {
  try {
    const customerId = `gid://shopify/Customer/${req.params.id}`;

    console.log(`\n👥 Fetching customer: ${customerId}`);

    const customer = await getCustomerById(customerId);

    if (!customer) {
      return res.status(404).json({
        success: false,
        error: 'Customer not found',
      });
    }

    res.json({
      success: true,
      data: customer,
    });
  } catch (error) {
    console.error('Error fetching customer:', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// Start server
app.listen(PORT, () => {
  console.log(`\n🚀 Shopify GraphQL API Server running on http://localhost:${PORT}`);
  console.log(`\nAPI Endpoints:`);
  console.log(`  GET /api/products - Fetch all products`);
  console.log(`  GET /api/products/:id - Fetch single product`);
  console.log(`  GET /api/products/search?q=query - Search products`);
  console.log(`  GET /api/orders - Fetch all orders`);
  console.log(`  GET /api/orders/:id - Fetch single order`);
  console.log(`  GET /api/customers - Fetch all customers`);
  console.log(`  GET /api/customers/:id - Fetch single customer\n`);
});

Step 4: Testing

Test 1: Start Server

bash
npm run dev
```

Expected output:
```
🚀 Shopify GraphQL API Server running on http://localhost:3000

API Endpoints:
  GET /api/products - Fetch all products
  GET /api/products/:id - Fetch single product
  ...

Test 2: Fetch All Products

bash
curl "http://localhost:3000/api/products?limit=5"

Expected response:

json
{
  "success": true,
  "count": 5,
  "data": [
    {
      "id": "gid://shopify/Product/1234567890",
      "title": "Example Product",
      "handle": "example-product",
      "status": "ACTIVE",
      "totalInventory": 100,
      "variants": {
        "edges": [...]
      }
    }
  ]
}
```

Check console for query cost:
```
📦 Fetching products (limit: 5)...
Query cost: 52/1000
Fetched 5 items (total: 5)
✓ All pages fetched

Test 3: Fetch Single Product by ID

bash
curl "http://localhost:3000/api/products/1234567890"

Test 4: Search Products

bash
curl "http://localhost:3000/api/products/search?q=shirt&limit=10"

Test 5: Fetch Orders

bash
# All orders
curl "http://localhost:3000/api/orders?limit=10"

# Orders by date range
curl "http://localhost:3000/api/orders?startDate=2024-01-01&endDate=2024-03-31"

Test 6: Test with GraphiQL (Recommended)

If you have Shopify GraphiQL app installed:

  1. Go to Shopify Admin → Apps → GraphiQL
  2. Paste this query:
graphql
{
  products(first: 3) {
    edges {
      node {
        id
        title
        totalInventory
        variants(first: 3) {
          edges {
            node {
              id
              sku
              price
            }
          }
        }
      }
    }
    pageInfo {
      hasNextPage
    }
  }
}
  1. Click Run and verify response

Test 7: Test Rate Limiting

Run multiple requests quickly:

bash
for i in {1..10}; do
  curl "http://localhost:3000/api/products?limit=50" &
done
```

Check console for rate limit warnings:
```
Query cost: 152/1000
Query cost: 304/950
⚠️  Approaching rate limit, consider slowing requests
⏳ Waiting 2.3s for rate limit...

Testing Checklist:

  • ✓ Server starts without errors
  • ✓ Products fetch successfully
  • ✓ Pagination works correctly
  • ✓ Query cost appears in logs
  • ✓ Rate limiting triggers when needed
  • ✓ Single product by ID works
  • ✓ Search returns filtered results
  • ✓ Orders fetch with line items
  • ✓ Customers fetch with addresses
  • ✓ Error handling works

Common Errors & Troubleshooting

Error 1: “Access denied for field” or Missing Data

Problem: GraphQL query returns null for certain fields or throws access errors.

Solution: API access token lacks required scopes.

Check current scopes: In Shopify Admin → Settings → Apps → Your Custom App → API credentials

Common missing scopes:

Field Required Scope
products.variants.inventoryQuantity read_inventory
orders.customer read_customers
orders.transactions read_orders
customers.metafields read_customer_metafields

Fix: Add missing scopes:

  1. Go to app settings → Configuration
  2. Click Configure Admin API scopes
  3. Add required scopes:
    • read_products
    • read_inventory
    • read_orders
    • read_customers
  4. SaveInstall app (reinstall with new scopes)
  5. Generate new access token

Update .env with new token:

env
SHOPIFY_ACCESS_TOKEN=shpat_new_token_here

Test scope permissions:

javascript
// Query to check available operations
const TEST_QUERY = `
  {
    shop {
      name
      features {
        storefront
        multiLocation
      }
    }
  }
`;

const response = await executeQuery(TEST_QUERY);
console.log('Shop accessible:', response.shop.name);

Error 2: “Query cost exceeds maximum” or 429 Rate Limit Errors

Problem: GraphQL query rejected with cost exceeded or rate limit error.

Solution: Query requests too much data at once. Reduce query complexity.

Identify expensive queries:

Check query cost in response:

javascript
// Cost info is in extensions
const cost = response.extensions?.cost;
console.log('Query cost:', cost.actualQueryCost);
console.log('Max available:', cost.throttleStatus.currentlyAvailable);

Reduce query cost strategies:

1. Fetch fewer items per request:

javascript
// Instead of:
products(first: 250) { ... } // Cost: ~500

// Use:
products(first: 50) { ... }  // Cost: ~100

2. Remove expensive nested fields:

javascript
// Expensive - fetches inventory for all locations
variant {
  inventoryItem {
    inventoryLevels(first: 50) { ... }
  }
}

// Cheaper - fetch only what's needed
variant {
  inventoryQuantity
}

3. Use pagination instead of bulk:

javascript
// Instead of single massive query, paginate:
async function fetchInBatches() {
  let cursor = null;
  
  for (let i = 0; i < 5; i++) {
    const batch = await executeQuery(QUERY, { first: 50, after: cursor });
    cursor = batch.products.pageInfo.endCursor;
    
    // Wait between batches
    await new Promise(r => setTimeout(r, 1000));
  }
}

4. Implement cost-aware fetching:

javascript
async function fetchWithCostLimit(query, variables) {
  const estimatedCost = 100; // Conservative estimate
  
  await rateLimiter.waitIfNeeded(estimatedCost);
  
  const response = await executeQuery(query, variables);
  
  // Update rate limiter with actual cost
  if (response.extensions?.cost) {
    rateLimiter.updateFromResponse(response.extensions.cost);
  }
  
  return response;
}

5. Use Shopify’s Bulk Operations for large datasets:

For fetching 10,000+ records, use Bulk Operations API:

graphql
mutation {
  bulkOperationRunQuery(
    query: """
    {
      products {
        edges {
          node {
            id
            title
          }
        }
      }
    }
    """
  ) {
    bulkOperation {
      id
      status
    }
    userErrors {
      field
      message
    }
  }
}
```

Poll for completion and download JSONL file.

### Error 3: "Invalid GID" or "node" Returns Null

**Problem:** Fetching by ID returns null or "Invalid GID" error.

**Solution:** Shopify uses Global IDs (GIDs) with specific format.

**Correct GID format:**
```
gid://shopify/Product/1234567890

Common mistakes:

javascript
// ❌ Wrong - numeric ID only
getProductById(1234567890)

// ❌ Wrong - missing schema
getProductById('Product/1234567890')

// ✅ Correct - full GID
getProductById('gid://shopify/Product/1234567890')

Convert numeric ID to GID:

javascript
function toShopifyGID(resourceType, id) {
  return `gid://shopify/${resourceType}/${id}`;
}

// Usage
const productGID = toShopifyGID('Product', 1234567890);
const orderGID = toShopifyGID('Order', 5555555);

Extract numeric ID from GID:

javascript
function extractIdFromGID(gid) {
  return gid.split('/').pop();
}

// Usage
const numericId = extractIdFromGID('gid://shopify/Product/1234567890');
// Returns: '1234567890'

Verify GID exists before querying:

javascript
async function safeGetProduct(id) {
  const gid = id.startsWith('gid://') ? id : toShopifyGID('Product', id);
  
  try {
    const product = await getProductById(gid);
    
    if (!product) {
      throw new Error(`Product ${gid} not found`);
    }
    
    return product;
  } catch (error) {
    console.error('Invalid product ID:', error.message);
    throw error;
  }
}

Security Checklist

Critical security practices for Shopify GraphQL integration:

  • Never expose access tokens client-side — Admin API tokens grant full store access. Keep tokens server-side only:
javascript
  // ❌ Never do this in frontend
  const token = 'shpat_abc123'; // Exposed in browser
  
  // ✅ Always proxy through backend
  app.get('/api/products', async (req, res) => {
    const products = await fetchFromShopify(); // Token stays on server
    res.json(products);
  });
```

- **Use minimal API scopes** — Only request permissions you actually need:
```For read-only dashboard: read_products, read_orders
  ❌ Avoid: write_products, write_customers (unless necessary)
  • Rotate access tokens regularly — Generate new tokens quarterly and revoke old ones in Shopify Admin.
  • Validate webhook signatures — When receiving Shopify webhooks, verify HMAC:
javascript
  import crypto from 'crypto';
  
  function verifyShopifyWebhook(body, hmacHeader, secret) {
    const hash = crypto
      .createHmac('sha256', secret)
      .update(body, 'utf8')
      .digest('base64');
    
    return crypto.timingSafeEqual(
      Buffer.from(hash),
      Buffer.from(hmacHeader)
    );
  }
  • Implement rate limiting on your endpoints — Prevent abuse of your API:
javascript
  import rateLimit from 'express-rate-limit';
  
  const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 100, // Max 100 requests per 15 minutes
  });
  
  app.use('/api/', limiter);
  • Sanitize user inputs in search queries — Prevent GraphQL injection:
javascript
  function sanitizeSearchQuery(input) {
    // Remove GraphQL special characters
    return input.replace(/[{}[\]()]/g, '').trim().slice(0, 100);
  }
  
  const safeQuery = sanitizeSearchQuery(req.query.q);
  • Log all API requests — Track usage patterns and detect anomalies:
javascript
  app.use((req, res, next) => {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
    next();
  });
  • Use HTTPS only — Never send API tokens over HTTP:
javascript
  if (process.env.NODE_ENV === 'production' && !req.secure) {
    return res.redirect('https://' + req.headers.host + req.url);
  }
  • Handle errors without exposing internals — Don’t leak stack traces or token fragments:
javascript
  app.use((error, req, res, next) => {
    console.error('Internal error:', error); // Log full error
    
    res.status(500).json({
      error: 'Internal server error', // Generic message to client
    });
  });
  • Implement query complexity limits — Reject overly complex queries client-side:
javascript
  function estimateComplexity(depth, breadth) {
    return depth * breadth;
  }
  
  if (estimateComplexity(queryDepth, queryBreadth) > 1000) {
    return res.status(400).json({ error: 'Query too complex' });
  }
  • Use environment-specific tokens — Separate dev/staging/production credentials:
env
  # .env.production
  SHOPIFY_ACCESS_TOKEN=shpat_prod_token
  
  # .env.development
  SHOPIFY_ACCESS_TOKEN=shpat_dev_token

Related Resources:

Need Expert Help?

Shopify GraphQL integrations require deep understanding of query optimization, rate limiting, and data modeling. If you need assistance building custom Shopify apps, optimizing API performance, or architecting scalable e-commerce solutions, schedule a consultation. We’ll help you build production-ready integrations that handle millions of products and orders.

Leave a Comment

Your email address will not be published. Required fields are marked *