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:
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:
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:
{
"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:
echo "node_modules/
.env
*.log" > .gitignore
Step 2: Configuration
.env — Store Shopify credentials securely:
# 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:
- Create Custom App:
- Log into Shopify Admin
- Go to Settings → Apps and sales channels
- Click Develop apps → Create an app
- Name: “GraphQL Data Integration”
- Configure API Scopes:
- Click Configure Admin API scopes
- Select required scopes:
read_productsread_ordersread_customersread_inventoryread_fulfillments
- Save
- 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:
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:
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:
// 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:
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:
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:
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:
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
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
curl "http://localhost:3000/api/products?limit=5"
Expected response:
{
"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
curl "http://localhost:3000/api/products/1234567890"
Test 4: Search Products
curl "http://localhost:3000/api/products/search?q=shirt&limit=10"
Test 5: Fetch Orders
# 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:
- Go to Shopify Admin → Apps → GraphiQL
- Paste this query:
{
products(first: 3) {
edges {
node {
id
title
totalInventory
variants(first: 3) {
edges {
node {
id
sku
price
}
}
}
}
}
pageInfo {
hasNextPage
}
}
}
- Click Run and verify response
Test 7: Test Rate Limiting
Run multiple requests quickly:
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:
- Go to app settings → Configuration
- Click Configure Admin API scopes
- Add required scopes:
read_productsread_inventoryread_ordersread_customers
- Save → Install app (reinstall with new scopes)
- Generate new access token
Update .env with new token:
SHOPIFY_ACCESS_TOKEN=shpat_new_token_here
Test scope permissions:
// 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:
// 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:
// Instead of:
products(first: 250) { ... } // Cost: ~500
// Use:
products(first: 50) { ... } // Cost: ~100
2. Remove expensive nested fields:
// 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:
// 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:
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:
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:
// ❌ 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:
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:
function extractIdFromGID(gid) {
return gid.split('/').pop();
}
// Usage
const numericId = extractIdFromGID('gid://shopify/Product/1234567890');
// Returns: '1234567890'
Verify GID exists before querying:
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:
// ❌ 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:
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:
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:
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:
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
next();
});
- Use HTTPS only — Never send API tokens over HTTP:
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:
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:
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.production
SHOPIFY_ACCESS_TOKEN=shpat_prod_token
# .env.development
SHOPIFY_ACCESS_TOKEN=shpat_dev_token
Related Resources:
- Custom Dashboard Using Pipedrive REST API – Similar API data fetching patterns
- Sync Salesforce Contacts with PostgreSQL – Database sync from API
- Webhooks to Trigger Automated Emails with SendGrid – Shopify webhook integration
- Building Audit Trails and Activity Logs – Track API requests
- Database Sharding Strategies for SaaS Applications – Scale Shopify data storage
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.

Huzaifa Asif is a dedicated software and integration specialist at TheSportsAngel, focused on making complex API and system integrations simple and actionable. With over 3+ years of hands-on experience in backend development, CRM/ERP connectivity, and third-party platform integrations, he transforms technical architecture into clear, step-by-step coding guides that both developers and non-technical users can follow.



