Claude Code skill pack for Shopify (30 skills)
Installation
Open Claude Code and run this command:
/plugin install shopify-pack@claude-code-plugins-plus
Use --global to install for all projects, or --project for current project only.
Skills (30)
Debug complex Shopify API issues using cost analysis, request tracing, webhook delivery inspection, and GraphQL introspection.
Shopify Advanced Troubleshooting
Overview
Deep debugging for complex Shopify API issues: cost analysis with debug headers, webhook delivery inspection, GraphQL query introspection, and systematic isolation of intermittent failures.
Prerequisites
- Access to Shopify admin and Partner Dashboard
- Familiarity with GraphQL and HTTP debugging
curlandjqavailable
Instructions
Step 1: GraphQL Cost Analysis
When queries THROTTLE unexpectedly, use the cost debug header:
# Get detailed per-field cost breakdown
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 metafields(first: 5) { edges { node { key value } } } } } } } } } }"
}' | jq '.extensions.cost'
Response shows why the cost is high:
{
"requestedQueryCost": 1552,
"actualQueryCost": 234,
"throttleStatus": {
"maximumAvailable": 1000.0,
"currentlyAvailable": 766.0,
"restoreRate": 50.0
}
}
Key: requestedQueryCost is first multiplied through nested connections. 50 products 20 variants (1 + 5 metafields) = high cost even if actual data is small.
Step 2: Trace a Specific Request
Every Shopify response includes X-Request-Id. Capture it for support:
# Capture full response headers and body
curl -v -X POST "https://$STORE/admin/api/2024-10/graphql.json" \
-H "X-Shopify-Access-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ shop { name } }"}' 2>&1 | tee /tmp/shopify-debug.txt
# Extract the request ID
grep -i "x-request-id" /tmp/shopify-debug.txt
Step 3: Webhook Delivery Inspection
Inspect webhook delivery status in the Partner Dashboard, or query via API:
// Check webhook subscription health
const WEBHOOK_STATUS = `{
webhookSubscriptions(first: 50) {
edges {
node {
id
topic
endpoint {
... on WebhookHttpEndpoint {
callbackUrl
}
}
format
apiVersion
createdAt
updatedAt
}
}
}
}`;
// Common webhook delivery issues:
// 1. Your endpoint returns non-200 — Shopify retries 19 times over 48 hours
// 2. Response takes > 5 seconds — Shopify considers it failed
// 3. Endpoint is HTTP (not HTTPS) — Choose between Shopify app architectures: embedded Remix app, headless storefront with Hydrogen, standalone integration, or theme app extension.
Shopify Architecture Variants
Overview
Four validated architecture patterns for building on Shopify. Choose based on your use case: embedded admin app, headless storefront, backend integration, or theme extension.
Prerequisites
- Clear understanding of what you're building
- Knowledge of your target merchants
- Understanding of Shopify's app ecosystem
Instructions
Variant A: Embedded Admin App (Remix)
Best for: Admin panel apps, merchant tools, dashboards, order management
When to use: You need to add functionality to the Shopify admin for merchants.
my-shopify-app/
├── app/
│ ├── routes/
│ │ ├── app._index.tsx # Dashboard (inside Shopify admin)
│ │ ├── app.products.tsx # Feature pages
│ │ ├── auth.$.tsx # OAuth handler
│ │ └── webhooks.tsx # Webhook receiver
│ ├── shopify.server.ts # @shopify/shopify-app-remix
│ └── root.tsx
├── extensions/ # Optional extensions
├── prisma/schema.prisma # Session + app data
├── shopify.app.toml
└── package.json
Key packages: @shopify/shopify-app-remix, @shopify/polaris, @shopify/app-bridge-react
API used: Admin GraphQL API (server-side via authenticate.admin())
Auth: OAuth with session token exchange (handled by the Remix adapter)
// Authenticated loader — runs server-side inside Shopify admin
export async function loader({ request }: LoaderFunctionArgs) {
const { admin } = await authenticate.admin(request);
const response = await admin.graphql(`{ shop { name plan { displayName } } }`);
return json(await response.json());
}
Variant B: Headless Storefront (Hydrogen)
Best for: Custom storefronts, unique shopping experiences, PWAs
When to use: You're building a custom frontend that replaces the Shopify Online Store.
my-hydrogen-store/
├── app/
│ ├── routes/
│ │ ├── ($locale)._index.tsx # Homepage
│ │ ├── ($locale).products.$handle.tsx # Product page
│ │ ├── ($locale).collections._index.tsx
│ │ ├── ($locale).cart.tsx # Cart page
│ │ └── ($locale).account.tsx # Customer account
│ ├── components/
│ │ ├── ProductCard.tsx
│ │ ├── Cart.tsx
│ │ └── Header.tsx
│ ├── lib/
│ │ └── shopify.ts # Storefront API client
│ └── root.tsx
├── public/
└── hydrogen.config.ts
Key packages: @shopify/hydrogen, @shopify/hydrogen-react, @shopify/remix-oxygen
API used: Storefront GraphQL API (public, no admin tokens needed)
Hosting:
Configure CI/CD pipelines for Shopify apps with GitHub Actions, API version testing, and Shopify CLI deployment.
Shopify CI Integration
Overview
Set up CI/CD pipelines for Shopify apps using GitHub Actions, including API version compatibility testing, Shopify CLI deployment, and extension validation.
Prerequisites
- GitHub repository with Actions enabled
- Shopify Partner account with CLI access
- Test store access token for integration tests
Instructions
Step 1: GitHub Actions Workflow
# .github/workflows/shopify-ci.yml
name: Shopify App CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
SHOPIFY_API_VERSION: "2024-10"
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test -- --coverage
env:
SHOPIFY_API_KEY: ${{ secrets.SHOPIFY_API_KEY }}
SHOPIFY_API_SECRET: ${{ secrets.SHOPIFY_API_SECRET }}
integration-test:
runs-on: ubuntu-latest
needs: lint-and-test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- name: Run Shopify integration tests
run: npm run test:integration
env:
SHOPIFY_STORE: ${{ secrets.SHOPIFY_TEST_STORE }}
SHOPIFY_ACCESS_TOKEN: ${{ secrets.SHOPIFY_TEST_TOKEN }}
SHOPIFY_API_KEY: ${{ secrets.SHOPIFY_API_KEY }}
SHOPIFY_API_SECRET: ${{ secrets.SHOPIFY_API_SECRET }}
api-version-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for deprecated API version
run: |
# Ensure we're not using an expired API version
VERSION=$(grep -r "apiVersion" src/ --include="*.ts" -h | head -1 | grep -oP '\d{4}-\d{2}')
echo "Using API version: $VERSION"
# Check if version is still supported
SUPPORTED=$(curl -sf -H "X-Shopify-Access-Token: ${{ secrets.SHOPIFY_TEST_TOKEN }}" \
"https://${{ secrets.SHOPIFY_TEST_STORE }}/admin/api/versions.json" \
| jq -r ".supported_versions[] | select(.handle == \"$VERSION\") | .supported")
if [ "$SUPPORTED" != "true" ]; then
echo "::warning::API version $VERSION is no longer supported!"
exit 1
fi
deploy:
runs-on: ubuntu-latest
needs: [lint-and-test, integration-test]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
Diagnose and fix common Shopify API errors including 401, 403, 422, 429, and GraphQL errors.
Shopify Common Errors
Overview
Quick-reference guide for the most common Shopify API errors with real error messages, causes, and fixes.
Prerequisites
- Shopify app with API credentials configured
- Access to application logs or console output
Instructions
Step 1: Identify the Error Type
Check whether the error is an HTTP status code error or a GraphQL userErrors response.
Step 2: Match Error Below and Apply Fix
401 Unauthorized
Actual Shopify Response:
{
"errors": "[API] Invalid API key or access token (unrecognized login or wrong password)"
}
Causes:
- Access token expired (merchant uninstalled and reinstalled)
- Wrong
X-Shopify-Access-Tokenheader - Using a Storefront API token for Admin API or vice versa
Fix:
# Verify token format:
# Admin API token: shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (32 hex chars)
# Storefront API token: different format, starts with shpat_ too
curl -s -o /dev/null -w "%{http_code}" \
-H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
"https://your-store.myshopify.com/admin/api/2024-10/shop.json"
# Should return 200
403 Forbidden
Actual Shopify Response:
{
"errors": "This action requires merchant approval for read_orders scope."
}
Cause: Your app's access token lacks the required scope.
Fix: Add the needed scope to your app config and re-trigger OAuth:
# shopify.app.toml
[access_scopes]
scopes = "read_products,write_products,read_orders,write_orders"
404 Not Found
Actual Shopify Response:
{
"errors": "Not Found"
}
Causes:
- Wrong API version in URL
- Resource was deleted
- Store domain is incorrect
Fix:
# Verify the API version exists
curl -s "https://your-store.myshopify.com/admin/api/2024-10/shop.json" \
-H "X-Shopify-Access-Token: $TOKEN"
# Check available API versions
curl -s "https://your-store.myshopify.com/admin/api/versions.json" \
-H "X-Shopify-Access-Token: $TOKEN"
422 Unprocessable Entity
Actual Shopify Responses:
{
"errors": {
"title": ["can't be blank"],
"handle": ["has already been taken"]
}
}
Manage Shopify products, variants, and collections using the GraphQL Admin API.
Shopify Products & Catalog Management
Overview
Primary workflow for Shopify: manage products, variants, collections, and inventory using the GraphQL Admin API. Covers CRUD operations with real API mutations and response shapes.
Prerequisites
- Completed
shopify-install-authsetup - Access scopes:
readproducts,writeproducts,readinventory,writeinventory - API version 2024-10 or later (ProductInput was split in this version)
Instructions
Step 1: Create a Product
// As of 2024-10, productCreate uses ProductCreateInput (not the old ProductInput)
const CREATE_PRODUCT = `
mutation productCreate($input: ProductCreateInput!) {
productCreate(product: $input) {
product {
id
title
handle
status
variants(first: 10) {
edges {
node {
id
title
price
sku
inventoryQuantity
}
}
}
}
userErrors {
field
message
code
}
}
}
`;
const response = await client.request(CREATE_PRODUCT, {
variables: {
input: {
title: "Premium Cotton T-Shirt",
descriptionHtml: "<p>Soft 100% organic cotton tee.</p>",
vendor: "My Brand",
productType: "Apparel",
tags: ["cotton", "organic", "summer"],
status: "DRAFT", // ACTIVE, DRAFT, or ARCHIVED
productOptions: [
{
name: "Size",
values: [{ name: "S" }, { name: "M" }, { name: "L" }, { name: "XL" }],
},
{
name: "Color",
values: [{ name: "Black" }, { name: "White" }, { name: "Navy" }],
},
],
},
},
});
// ALWAYS check userErrors — Shopify returns 200 even on validation failures
if (response.data.productCreate.userErrors.length > 0) {
console.error("Validation errors:", response.data.productCreate.userErrors);
// Example: [{ field: ["title"], message: "Title can't be blank", code: "BLANK" }]
}
Step 2: Update a Product
// 2024-10+: productUpdate uses ProductUpdateInput (separate from create)
const UPDATE_PRODUCT = `
mutation productUpdate($input: ProductUpdateInput!) {
productUpdate(product: $input) {
product {
id
title
status
updatedAt
}
userErrors {
field
message
}
}
}
`;
await client.request(UPDATE_PRODUCT, {
variables: {
input: {
id: "gid://shopify/Product/1234567890",
title: "Updated Product TitlManage Shopify orders, customers, and fulfillments using the GraphQL Admin API.
Shopify Orders & Customer Management
Overview
Secondary core workflow: manage orders, customers, and fulfillments. Query order data, process fulfillments, and handle customer records through the GraphQL Admin API.
Prerequisites
- Completed
shopify-install-authsetup - Access scopes:
readorders,writeorders,readcustomers,writecustomers,readfulfillments,writefulfillments - Familiarity with
shopify-core-workflow-a(products)
Instructions
Step 1: Query Orders
const QUERY_ORDERS = `
query orders($first: Int!, $query: String, $after: String) {
orders(first: $first, query: $query, after: $after, sortKey: CREATED_AT, reverse: true) {
edges {
node {
id
name # "#1001"
createdAt
displayFinancialStatus # PAID, PENDING, REFUNDED, etc.
displayFulfillmentStatus # FULFILLED, UNFULFILLED, PARTIALLY_FULFILLED
totalPriceSet {
shopMoney { amount currencyCode }
}
customer {
id
displayName
email
}
lineItems(first: 10) {
edges {
node {
title
quantity
variant {
id
sku
price
}
originalTotalSet {
shopMoney { amount currencyCode }
}
}
}
}
shippingAddress {
address1
city
province
country
zip
}
}
cursor
}
pageInfo { hasNextPage endCursor }
}
}
`;
// Shopify order query syntax:
// "financial_status:paid"
// "fulfillment_status:unfulfilled"
// "created_at:>2024-01-01"
// "name:#1001"
// "email:customer@example.com"
// "tag:rush"
const data = await client.request(QUERY_ORDERS, {
variables: {
first: 25,
query: "financial_status:paid AND fulfillment_status:unfulfilled",
},
});
Step 2: Create a Draft Order
const CREATE_DRAFT_ORDER = `
mutation draftOrderCreate($input: DraftOrderInput!) {
draftOrderCreate(input: $input) {
draftOrder {
id
name
totalPriceSet {
shopMoney { amount currencyCode }
}
invoiceUrl
}
userErrors {
field
message
}
}
}
`;
await client.request(CREATE_DRAFT_ORDER, {
variables: {
input: {
lineItems: [
{
variantId: "gid://shopify/ProductVariant/12345",
quantity: 2,
},
{
Optimize Shopify app costs through plan selection, API usage monitoring, and Shopify Plus upgrade analysis.
Shopify Cost Tuning
Overview
Optimize Shopify app and API costs through plan analysis, API usage monitoring, and strategies to minimize billable API calls. Covers Shopify store plans, Partner app billing, and API efficiency.
Prerequisites
- Access to Shopify Partner Dashboard for app billing
- Understanding of current API usage patterns
- Knowledge of merchant's Shopify plan
Instructions
Step 1: Understand Shopify Plan Rate Limits
API rate limits are determined by the merchant's plan, not your app:
| Merchant Plan | REST Bucket | REST Leak Rate | GraphQL Points | GraphQL Restore |
|---|---|---|---|---|
| Basic Shopify | 40 requests | 2/second | 1,000 points | 50/second |
| Shopify | 40 requests | 2/second | 1,000 points | 50/second |
| Advanced | 40 requests | 2/second | 1,000 points | 50/second |
| Shopify Plus | 80 requests | 4/second | 2,000 points | 100/second |
Key insight: Upgrading from Basic to Advanced doesn't help rate limits. Only Plus doubles them.
Step 2: App Billing API
If your app charges merchants, use the GraphQL App Billing API:
// Create a recurring charge
const CREATE_SUBSCRIPTION = `
mutation appSubscriptionCreate(
$name: String!,
$lineItems: [AppSubscriptionLineItemInput!]!,
$returnUrl: URL!,
$test: Boolean
) {
appSubscriptionCreate(
name: $name,
lineItems: $lineItems,
returnUrl: $returnUrl,
test: $test
) {
appSubscription {
id
status
}
confirmationUrl
userErrors { field message }
}
}
`;
const response = await client.request(CREATE_SUBSCRIPTION, {
variables: {
name: "Pro Plan",
returnUrl: "https://your-app.com/billing/callback",
test: process.env.NODE_ENV !== "production", // test charges in dev
lineItems: [
{
plan: {
appRecurringPricingDetails: {
price: { amount: 9.99, currencyCode: "USD" },
interval: "EVERY_30_DAYS",
},
},
},
],
},
});
// Redirect merchant to confirmationUrl to approve the charge
Step 3: Monitor API Usage
class ShopifyUsageTracker {
private graphqlCosts: number[] = [];
private restCalls: number = 0;
private startOfPeriod: Date = new Date();
trackGraphqlCost(extensions: any): void {
if (extensions?.cost?.actualQueryCost) {
this.graphqlCosts.push(extensions.cost.actualQueryCost);
}
}
trackRestCall(): void {
this.restCalls++;
}
getReport(): UsageReport {
Handle Shopify customer PII, implement GDPR/CCPA compliance, and manage data retention with Shopify's mandatory privacy webhooks.
Shopify Data Handling
Overview
Handle customer PII correctly when building Shopify apps. Covers the mandatory GDPR webhooks, data minimization, and the specific privacy requirements Shopify enforces for App Store submission.
Prerequisites
- Understanding of GDPR/CCPA requirements
- Shopify app with webhook handling configured
- Database for storing and deleting customer data
Instructions
Step 1: Understand What Data Shopify Shares
When a merchant grants your app access, you may receive:
| Data Type | Source | Sensitivity | Retention Obligation |
|---|---|---|---|
| Customer email, name, phone | read_customers scope |
PII — encrypt at rest | Delete on customers/redact |
| Shipping addresses | read_orders scope |
PII — encrypt at rest | Delete on customers/redact |
| Order details (amounts, items) | read_orders scope |
Business data | Delete on shop/redact |
| Product data | read_products scope |
Public | Delete on shop/redact |
| Shop owner email | read_shop scope |
PII | Delete on shop/redact |
Step 2: Implement Mandatory Privacy Webhooks
Shopify requires three GDPR webhooks for App Store apps. Your app will be rejected without them.
// 1. customers/data_request — Customer requests their data
// Shopify sends this when a customer asks the merchant for their data
async function handleCustomerDataRequest(payload: {
shop_domain: string;
customer: { id: number; email: string; phone: string };
orders_requested: number[];
data_request: { id: number };
}): Promise<void> {
// Collect all data you store about this customer
const customerData = await db.customerRecords.findMany({
where: {
shopDomain: payload.shop_domain,
shopifyCustomerId: String(payload.customer.id),
},
});
const orderData = await db.orderRecords.findMany({
where: {
shopDomain: payload.shop_domain,
shopifyOrderId: { in: payload.orders_requested.map(String) },
},
});
// You have 30 days to respond
// Email the data to the merchant (or make it available via your app)
await sendDataExport({
requestId: payload.data_request.id,
shop: payload.shop_domain,
customer: customerData,
orders: orderData,
});
}
// 2. customers/redact — Delete specific customer's data
async function handleCustomerRedact(payload: {
shop_domain: string;
customer: { id: number; email: string; phone: string };
orders_to_redact: number[];
}): Promise<void> {
// Delete ALL Collect Shopify debug evidence including API versions, scopes, rate limit state, and request logs.
Shopify Debug Bundle
Overview
Collect all diagnostic information needed for Shopify support tickets: API version compatibility, access scopes, rate limit state, recent errors, and connectivity checks.
Prerequisites
- Shopify access token (
shpat_xxx) available curlandjqinstalled- Store domain known (
*.myshopify.com)
Instructions
Step 1: Create Debug Bundle Script
#!/bin/bash
# shopify-debug-bundle.sh
set -euo pipefail
STORE="${SHOPIFY_STORE:-your-store.myshopify.com}"
TOKEN="${SHOPIFY_ACCESS_TOKEN}"
VERSION="${SHOPIFY_API_VERSION:-2024-10}"
BUNDLE_DIR="shopify-debug-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BUNDLE_DIR"
echo "=== Shopify Debug Bundle ===" | tee "$BUNDLE_DIR/summary.txt"
echo "Store: $STORE" | tee -a "$BUNDLE_DIR/summary.txt"
echo "API Version: $VERSION" | tee -a "$BUNDLE_DIR/summary.txt"
echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" | tee -a "$BUNDLE_DIR/summary.txt"
echo "---" | tee -a "$BUNDLE_DIR/summary.txt"
Step 2: Collect API State
# Shop info and plan
echo "--- Shop Info ---" >> "$BUNDLE_DIR/summary.txt"
curl -sf -H "X-Shopify-Access-Token: $TOKEN" \
"https://$STORE/admin/api/$VERSION/shop.json" \
| jq '{name: .shop.name, plan: .shop.plan_name, domain: .shop.domain, timezone: .shop.iana_timezone}' \
>> "$BUNDLE_DIR/summary.txt" 2>&1 || echo "FAILED: shop.json" >> "$BUNDLE_DIR/summary.txt"
# Granted access scopes
echo "--- Access Scopes ---" >> "$BUNDLE_DIR/summary.txt"
curl -sf -H "X-Shopify-Access-Token: $TOKEN" \
"https://$STORE/admin/oauth/access_scopes.json" \
| jq '.access_scopes[].handle' \
>> "$BUNDLE_DIR/summary.txt" 2>&1 || echo "FAILED: scopes" >> "$BUNDLE_DIR/summary.txt"
# Supported API versions
echo "--- API Versions ---" >> "$BUNDLE_DIR/summary.txt"
curl -sf -H "X-Shopify-Access-Token: $TOKEN" \
"https://$STORE/admin/api/versions.json" \
| jq '.supported_versions[] | {handle, display_name, latest, supported}' \
>> "$BUNDLE_DIR/summary.txt" 2>&1 || echo "FAILED: versions" >> "$BUNDLE_DIR/summary.txt"
Step 3: Test Rate Limit State
# GraphQL rate limit check — inspects cost headers
echo "--- Rate Limit State ---" >> "$BUNDLE_DIR/summary.txt"
curl -sf -H "X-Shopify-Access-Token: $TOKEN" \
-H "Content-Type: application/json" \
-Deploy Shopify apps to Vercel, Fly.
Shopify Deploy Integration
Overview
Deploy Shopify apps to popular hosting platforms. Covers environment configuration, webhook URL setup, and Shopify CLI deployment for extensions.
Prerequisites
- Shopify app tested locally with
shopify app dev - Platform CLI installed (vercel, fly, or gcloud)
- Production API credentials ready
shopify.app.tomlconfigured
Instructions
Step 1: Deploy App with Shopify CLI
# Shopify CLI handles extension deployment and app config sync
shopify app deploy
# This uploads:
# - Theme app extensions
# - Function extensions
# - App configuration (URLs, scopes, webhooks)
# But NOT your web app — you host that separately
Step 2: Vercel Deployment
# Set environment variables
vercel env add SHOPIFY_API_KEY production
vercel env add SHOPIFY_API_SECRET production
vercel env add SHOPIFY_SCOPES production
vercel env add SHOPIFY_APP_URL production
# Deploy
vercel --prod
// vercel.json
{
"framework": "remix",
"env": {
"SHOPIFY_API_KEY": "@shopify-api-key",
"SHOPIFY_API_SECRET": "@shopify-api-secret"
},
"headers": [
{
"source": "/webhooks(.*)",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" }
]
}
],
"functions": {
"app/**/*.ts": { "maxDuration": 25 }
}
}
Update shopify.app.toml with your Vercel URL:
[auth]
redirect_urls = [
"https://your-app.vercel.app/auth/callback"
]
application_url = "https://your-app.vercel.app"
Step 3: Fly.io Deployment
# fly.toml
app = "my-shopify-app"
primary_region = "iad"
[env]
NODE_ENV = "production"
SHOPIFY_API_VERSION = "2024-10"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 1
[checks]
[checks.health]
port = 3000
type = "http"
interval = "15s"
timeout = "2s"
path = "/health"
# Set secrets (never in fly.toml)
fly secrets set \
SHOPIFY_API_KEY="your_key" \
SHOPIFY_API_SECRET="your_secret" \
SHOPIFY_ACCESS_TOKEN="shpat_xxx"
# Deploy
fly deploy
# Check health
fly status
curl https://my-shopify-app.fly.dev/health
Step 4: Google Cloud Run Deployment
# Dockerfile
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npmImplement Shopify Plus access control patterns with staff permissions, multi-location management, and Shopify Organization features.
Shopify Enterprise RBAC
Overview
Implement role-based access control for Shopify Plus apps using Shopify's staff member permissions, multi-location features, and Organization-level access.
Prerequisites
- Shopify Plus store (for Organization features)
- Understanding of Shopify's staff permission model
read_usersscope for querying staff permissions
Instructions
Step 1: Query Staff Member Permissions
// Query staff members and their permissions via GraphQL
const STAFF_QUERY = `{
staffMembers(first: 50) {
edges {
node {
id
email
firstName
lastName
isShopOwner
active
locale
permissions: accessScopes {
handle
description
}
}
}
}
}`;
// Staff permissions match app access scopes:
// "read_products", "write_products", "read_orders", etc.
// A staff member can only use app features matching their store permissions
Step 2: App-Level Role Mapping
Map Shopify staff permissions to your app's roles:
type AppRole = "admin" | "manager" | "viewer" | "fulfillment";
interface RoleMapping {
role: AppRole;
requiredScopes: string[];
allowedActions: string[];
}
const ROLE_MAPPINGS: RoleMapping[] = [
{
role: "admin",
requiredScopes: ["write_products", "write_orders", "write_customers"],
allowedActions: ["*"],
},
{
role: "manager",
requiredScopes: ["write_products", "read_orders"],
allowedActions: ["manage_products", "view_orders", "view_analytics"],
},
{
role: "fulfillment",
requiredScopes: ["read_orders", "write_fulfillments"],
allowedActions: ["view_orders", "create_fulfillment", "update_tracking"],
},
{
role: "viewer",
requiredScopes: ["read_products"],
allowedActions: ["view_products", "view_analytics"],
},
];
function determineRole(staffScopes: string[]): AppRole {
// Find the highest-privilege role the staff member qualifies for
for (const mapping of ROLE_MAPPINGS) {
if (mapping.requiredScopes.every((s) => staffScopes.includes(s))) {
return mapping.role;
}
}
return "viewer"; // fallback
}
function canPerformAction(role: AppRole, action: string): boolean {
const mapping = ROLE_MAPPINGS.find((m) => m.role === role);
if (!mapping) return false;
return mapping.allowedActions.includes("*") || mapping.allowedActions.includes(action);
}
Step 3: Embedded App Permission Middleware
// In an embedded ShopiCreate a minimal working Shopify app that queries products via GraphQL Admin API.
Shopify Hello World
Overview
Minimal working example: query your store's products using the Shopify GraphQL Admin API. Uses @shopify/shopify-api with a custom app access token for zero-friction setup.
Prerequisites
- Completed
shopify-install-authsetup - A Shopify development store
- An Admin API access token (
shpat_xxx) from Settings > Apps > Develop apps
Instructions
Step 1: Create Project
mkdir shopify-hello-world && cd shopify-hello-world
npm init -y
npm install @shopify/shopify-api dotenv
Step 2: Configure Environment
# .env
SHOPIFY_STORE=your-store.myshopify.com
SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SHOPIFY_API_KEY=your_api_key
SHOPIFY_API_SECRET=your_api_secret
Step 3: Write the Hello World Script
// hello-shopify.ts
import "@shopify/shopify-api/adapters/node";
import { shopifyApi } from "@shopify/shopify-api";
import "dotenv/config";
const shopify = shopifyApi({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
hostName: "localhost",
apiVersion: "2024-10",
isCustomStoreApp: true,
adminApiAccessToken: process.env.SHOPIFY_ACCESS_TOKEN!,
});
async function main() {
const session = shopify.session.customAppSession(
process.env.SHOPIFY_STORE!
);
const client = new shopify.clients.Graphql({ session });
// Query shop info
const shopInfo = await client.request(`{
shop {
name
currencyCode
primaryDomain { url }
}
}`);
console.log("Store:", shopInfo.data.shop.name);
console.log("Currency:", shopInfo.data.shop.currencyCode);
// Query first 5 products
const products = await client.request(`{
products(first: 5) {
edges {
node {
id
title
status
totalInventory
variants(first: 3) {
edges {
node {
title
price
sku
inventoryQuantity
}
}
}
}
}
}
}`);
console.log("\nProducts:");
for (const edge of products.data.products.edges) {
const p = edge.node;
console.log(` - ${p.title} (${p.status}, ${p.totalInventory} in stock)`);
for (const v of p.variants.edges) {
console.log(` Variant: ${v.node.title} — $${v.node.price} (SKU: ${v.node.sku})`);
}
}
console.log("\nSuccess! Your Shopify connection is working.");
}
main().catch((err) => {
console.error("Failed:", err.message);
if (err.response) {
console.error("Response:", JSON.stringify(err.response.body, null, 2));
}
process.exit(1);
});
Step
Execute Shopify incident response with triage using Shopify status page, API health checks, and rate limit diagnosis.
Shopify Incident Runbook
Overview
Rapid incident response for Shopify API outages, authentication failures, and rate limit emergencies. Distinguish between Shopify-side issues and your app's integration issues.
Prerequisites
- Access to Shopify admin and status page
- Application logs and metrics
- Communication channels (Slack, PagerDuty)
Instructions
Step 1: Quick Triage (First 5 Minutes)
#!/bin/bash
echo "=== SHOPIFY INCIDENT TRIAGE ==="
echo "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
# 1. Is Shopify itself down?
echo ""
echo "--- Shopify Status ---"
echo "Check: https://www.shopifystatus.com"
echo "API Status: https://www.shopifystatus.com/api/v2/status.json"
curl -sf "https://www.shopifystatus.com/api/v2/status.json" 2>/dev/null \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(f'Overall: {d[\"status\"][\"description\"]}')" \
2>/dev/null || echo "Could not reach status page"
# 2. Can we reach the Shopify API?
echo ""
echo "--- API Connectivity ---"
echo -n "Admin API: "
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
-H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
"https://$SHOPIFY_STORE/admin/api/2024-10/shop.json" 2>/dev/null)
echo "$HTTP_CODE"
# 3. Rate limit state
echo ""
echo "--- Rate Limit State ---"
curl -sI -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
"https://$SHOPIFY_STORE/admin/api/2024-10/shop.json" 2>/dev/null \
| grep -i "x-shopify-shop-api-call-limit"
# 4. GraphQL rate limit
echo ""
echo "--- GraphQL Throttle ---"
curl -sf -H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ shop { name } }"}' \
"https://$SHOPIFY_STORE/admin/api/2024-10/graphql.json" 2>/dev/null \
| python3 -c "
import json,sys
d=json.load(sys.stdin)
t=d.get('extensions',{}).get('cost',{}).get('throttleStatus',{})
print(f'Available: {t.get(\"currentlyAvailable\",\"?\")}/{t.get(\"maximumAvailable\",\"?\")}')
print(f'Restore rate: {t.get(\"restoreRate\",\"?\")}/sec')
" 2>/dev/null || echo "Could not query"
Step 2: Decision Tree
Is Shopifystatus.com showing an incident?
├── YES → Shopify-side outage
│ ├── Enable graceful degradation / cached responses
│ ├── Notify stakeholders: "Shopify is experiencing issues"
│ └── Monitor status page for resolution
│
└── NO → Likely your integration
├── HTTP 401? → Token expired or revoked
│ └── Check: WasInstall and configure Shopify app authentication with OAuth, session tokens, and the @shopify/shopify-api SDK.
Shopify Install & Auth
Overview
Set up Shopify app authentication using the official @shopify/shopify-api library. Covers OAuth flow, session token exchange, custom app tokens, and Storefront API access.
Prerequisites
- Node.js 18+ (the
@shopify/shopify-apiv9+ requires it) - A Shopify Partner account at https://partners.shopify.com
- An app created in the Partner Dashboard with API credentials
- A development store for testing
Instructions
Step 1: Install the Shopify API Library
# Core library + Node.js runtime adapter
npm install @shopify/shopify-api @shopify/shopify-app-remix
# Or for standalone Node apps:
npm install @shopify/shopify-api @shopify/shopify-app-express
# For Remix (recommended by Shopify):
npm install @shopify/shopify-app-remix @shopify/app-bridge-react
Step 2: Configure Environment Variables
Create a .env file (add to .gitignore immediately):
# .env — NEVER commit this file
SHOPIFY_API_KEY=your_app_api_key
SHOPIFY_API_SECRET=your_app_api_secret
SHOPIFY_SCOPES=read_products,write_products,read_orders,write_orders
SHOPIFY_APP_URL=https://your-app.example.com
SHOPIFY_HOST_NAME=your-app.example.com
# For custom/private apps only:
SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxxx
# API version — use a stable quarterly release
SHOPIFY_API_VERSION=2024-10
# .gitignore — add these immediately
.env
.env.local
.env.*.local
Step 3: Initialize the Shopify API Library
// src/shopify.ts
import "@shopify/shopify-api/adapters/node";
import { shopifyApi, LATEST_API_VERSION, Session } from "@shopify/shopify-api";
const shopify = shopifyApi({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
scopes: process.env.SHOPIFY_SCOPES!.split(","),
hostName: process.env.SHOPIFY_HOST_NAME!,
apiVersion: LATEST_API_VERSION, // or "2024-10"
isEmbeddedApp: true,
});
export default shopify;
Step 4: Implement OAuth Flow (Public Apps)
// routes/auth.ts — Express example
import express from "express";
import shopify from "../shopify";
const router = express.Router();
// Step 1: Begin OAuth — redirect merchant to Shopify
router.get("/auth", async (req, res) => {
const shop = req.query.shop as string;
// Generates the authorization URL with HMAC validation
const authRoute = await shopify.auth.begin({
shop: shopify.utils.sanitizeShop(shop, true)!,
callbackPath: "/auth/callback",
isOnline: false, // offline = long-lived token
rawRequest: req,
rawResponse: res,
});
});
// Step 2: Handle callback — exchange code for access tokIdentify and avoid Shopify API anti-patterns: ignoring userErrors, wrong API version, REST instead of GraphQL, missing GDPR webhooks, and webhook timeout issues.
Shopify Known Pitfalls
Overview
The 10 most common mistakes when building Shopify apps, with real API examples showing the wrong way and the right way.
Prerequisites
- Shopify app codebase to review
- Understanding of GraphQL Admin API patterns
Instructions
Pitfall #1: Not Checking userErrors (The #1 Mistake)
Shopify GraphQL mutations return HTTP 200 even when they fail. The errors are in userErrors.
// WRONG — assumes 200 means success
const response = await client.request(PRODUCT_CREATE, { variables });
const product = response.data.productCreate.product; // null!
console.log(product.title); // TypeError: Cannot read property 'title' of null
// RIGHT — always check userErrors
const response = await client.request(PRODUCT_CREATE, { variables });
const { product, userErrors } = response.data.productCreate;
if (userErrors.length > 0) {
console.error("Shopify validation failed:", userErrors);
// [{ field: ["title"], message: "Title can't be blank", code: "BLANK" }]
throw new ShopifyValidationError(userErrors);
}
console.log(product.title); // Safe
Pitfall #2: Using REST When GraphQL Is Required
REST Admin API is legacy as of October 2024. New public apps after April 2025 must use GraphQL.
// WRONG — REST API (legacy, higher bandwidth, returns all fields)
const { body } = await restClient.get({ path: "products", query: { limit: 250 } });
// Returns EVERYTHING: body_html, template_suffix, published_scope...
// RIGHT — GraphQL (get only what you need)
const response = await graphqlClient.request(`{
products(first: 50) {
edges { node { id title status } }
pageInfo { hasNextPage endCursor }
}
}`);
Pitfall #3: Ignoring API Version Deprecation
Shopify deprecates API versions ~12 months after release. Your app will break silently when your version is removed.
// WRONG — hardcoded old version, no monitoring
const shopify = shopifyApi({ apiVersion: "2023-04" }); // DEAD version
// RIGHT — use recent stable version, monitor deprecation
const shopify = shopifyApi({ apiVersion: "2024-10" });
// Monitor for deprecation warnings in responses
function checkDeprecation(headers: Headers): void {
const warning = headers.get("x-shopify-api-deprecated-reason");
if (warning) {
console.warn(`[DEPRECATION] ${warning}`);
// Alert team to upgrade
}
}
Pitfall #4: Missing Mandatory GDPR Webhooks
Your app will be rejected from the App Store without these three webhooks.
// WRONG — no GDPR handlers
// shopify.app.toml has no webhook subscriptions
// App Store review: REJECTED
// Load test Shopify integrations respecting API rate limits, plan capacity with k6, and scale for Shopify Plus burst events (flash sales, BFCM).
Shopify Load & Scale
Overview
Load test Shopify app integrations while respecting API rate limits. Plan capacity for high-traffic events like Black Friday / Cyber Monday (BFCM).
Prerequisites
- k6 load testing tool installed (
brew install k6) - Test store with API access (never load test production)
- Understanding of Shopify rate limits per plan
Instructions
Step 1: Understand Capacity Constraints
Your app's throughput is bounded by Shopify's rate limits, not your infrastructure:
| Plan | GraphQL Points | Restore Rate | Max Sustained QPS | Burst Capacity |
|---|---|---|---|---|
| Standard | 1,000 | 50/sec | ~10 queries/sec | 1,000 points burst |
| Shopify Plus | 2,000 | 100/sec | ~20 queries/sec | 2,000 points burst |
A typical product query costs 10-50 points. At 50 points/query, Standard supports ~1 query/second sustained.
Step 2: k6 Load Test Script
// shopify-load-test.js
import http from "k6/http";
import { check, sleep } from "k6";
import { Rate, Counter, Trend } from "k6/metrics";
// Custom metrics
const shopifyErrors = new Rate("shopify_errors");
const throttledRequests = new Counter("shopify_throttled");
const queryCost = new Trend("shopify_query_cost");
export const options = {
stages: [
{ duration: "1m", target: 2 }, // Warm up — 2 VUs
{ duration: "3m", target: 5 }, // Normal load
{ duration: "2m", target: 10 }, // Peak load
{ duration: "1m", target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ["p(95)<2000"], // 95% under 2s
shopify_errors: ["rate<0.05"], // < 5% error rate
shopify_throttled: ["count<10"], // < 10 throttled requests
},
};
const STORE = __ENV.SHOPIFY_STORE;
const TOKEN = __ENV.SHOPIFY_ACCESS_TOKEN;
const API_VERSION = "2024-10";
export default function () {
const query = JSON.stringify({
query: `{
products(first: 10) {
edges {
node { id title status totalInventory }
}
}
}`,
});
const res = http.post(
`https://${STORE}/admin/api/${API_VERSION}/graphql.json`,
query,
{
headers: {
"Content-Type": "application/json",
"X-Shopify-Access-Token": TOKEN,
},
}
);
const body = JSON.parse(res.body);
// Track GraphQL-level throttling (returns 200 with THROTTLED error)
const isThrottled = body.errors?.some(
(e) => e.extensions?.code === "THROTTLED"
);
if (isThrottled) {
throttledRequests.add(1);
// Wait for restore rate to refill
const avaiConfigure Shopify local development with Shopify CLI, hot reload, and ngrok tunneling.
Shopify Local Dev Loop
Overview
Set up a fast, reproducible local development workflow using Shopify CLI, ngrok tunneling for webhooks, and Vitest for testing against the Shopify API.
Prerequisites
- Completed
shopify-install-authsetup - Node.js 18+ with npm/pnpm
- Shopify CLI 3.x (
npm install -g @shopify/cli) - A Shopify Partner account and development store
Instructions
Step 1: Scaffold with Shopify CLI
# Create a new Remix-based Shopify app (recommended)
shopify app init
# Or scaffold manually
mkdir my-shopify-app && cd my-shopify-app
npm init -y
npm install @shopify/shopify-api @shopify/shopify-app-remix \
@shopify/app-bridge-react @remix-run/node @remix-run/react
Step 2: Project Structure
my-shopify-app/
├── app/
│ ├── routes/
│ │ ├── app._index.tsx # Main app page
│ │ ├── app.products.tsx # Products management
│ │ ├── auth.$.tsx # OAuth callback
│ │ └── webhooks.tsx # Webhook handler
│ ├── shopify.server.ts # Shopify API client setup
│ └── root.tsx
├── extensions/ # Theme app extensions
├── shopify.app.toml # App configuration
├── .env # Local secrets (git-ignored)
├── .env.example # Template for team
└── package.json
Step 3: Configure shopify.app.toml
# shopify.app.toml — central app configuration
name = "My App"
client_id = "your_api_key_here"
[access_scopes]
scopes = "read_products,write_products,read_orders"
[auth]
redirect_urls = [
"https://localhost/auth/callback",
"https://localhost/auth/shopify/callback",
]
[webhooks]
api_version = "2024-10"
[webhooks.subscriptions]
# Mandatory GDPR webhooks
[[webhooks.subscriptions]]
topics = ["customers/data_request"]
uri = "/webhooks"
[[webhooks.subscriptions]]
topics = ["customers/redact"]
uri = "/webhooks"
[[webhooks.subscriptions]]
topics = ["shop/redact"]
uri = "/webhooks"
Step 4: Start Dev Server with Tunnel
# Shopify CLI handles ngrok tunnel + OAuth automatically
shopify app dev
# This will:
# 1. Start your app on localhost:3000
# 2. Create an ngrok tunnel
# 3. Update your app URLs in Partner Dashboard
# 4. Open your app in the dev store admin
# 5. Hot reload on file changes
Step 5: Set Up Testing
// tests/shopify-client.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock the Shopify API client
vi.mock("@shopify/shopify-api", () => ({
shopifyApi: vi.fn(() => ({
clients: {
Graphql: vi.fn().mockImplementation((Migrate e-commerce data to Shopify using bulk operations, product imports, and the strangler fig pattern for gradual platform migration.
Shopify Migration Deep Dive
Overview
Migrate product catalogs, customers, and orders to Shopify using the GraphQL Admin API bulk mutations, CSV imports, and incremental migration patterns.
Prerequisites
- Source platform data exported (CSV, JSON, or API access)
- Shopify store with appropriate access scopes
- Scopes needed:
writeproducts,writecustomers,writeorders,writeinventory
Instructions
Step 1: Assess Migration Scope
| Data Type | Shopify Import Method | Complexity |
|---|---|---|
| Products + variants | productSet mutation (upsert) |
Low |
| Product images | productCreateMedia mutation |
Low |
| Customers | Customer CSV import or customerCreate |
Medium |
| Historical orders | draftOrderCreate + draftOrderComplete |
High |
| Inventory levels | inventorySetQuantities mutation |
Medium |
| Collections | collectionCreate mutation |
Low |
| Redirects (URLs) | urlRedirectCreate mutation |
Low |
| Metafields | Included in product/customer mutations | Medium |
Step 2: Bulk Product Import with productSet
productSet is idempotent — it creates or updates based on handle. Perfect for migrations.
const PRODUCT_SET = `
mutation productSet($input: ProductSetInput!) {
productSet(input: $input) {
product {
id
title
handle
variants(first: 50) {
edges {
node { id sku price inventoryQuantity }
}
}
}
userErrors { field message code }
}
}
`;
// Migrate products in batches
async function migrateProducts(sourceProducts: SourceProduct[]): Promise<MigrationResult> {
const results: MigrationResult = { success: 0, errors: [] };
for (const product of sourceProducts) {
try {
const response = await client.request(PRODUCT_SET, {
variables: {
input: {
title: product.name,
handle: product.slug, // unique identifier for upsert
descriptionHtml: product.description,
vendor: product.brand,
productType: product.category,
tags: product.tags,
status: "DRAFT", // Keep as draft until verified
variants: product.variants.map((v) => ({
price: String(v.price),
sku: v.sku,
barcode: v.barcode,
optionValues: v.options.map((opt) => ({
optionNConfigure Shopify apps across development, staging, and production environments with separate stores, API credentials, and app instances.
Shopify Multi-Environment Setup
Overview
Configure Shopify apps with isolated development, staging, and production environments. Each environment uses a separate Shopify app instance, development store, and credentials.
Prerequisites
- Shopify Partner account
- Multiple development stores (free to create in Partner Dashboard)
- Secret management solution for production (Vault, AWS Secrets Manager, etc.)
Instructions
Step 1: Create Separate App Instances
Create one app per environment in your Partner Dashboard:
| Environment | App Name | Store | Purpose |
|---|---|---|---|
| Development | My App (Dev) | dev-store.myshopify.com | Local development |
| Staging | My App (Staging) | staging-store.myshopify.com | Pre-prod testing |
| Production | My App | live-store.myshopify.com | Live traffic |
Each app gets its own APIKEY, APISECRET, and ACCESS_TOKEN.
Step 2: Environment Configuration Files
# .env.development (git-ignored)
SHOPIFY_API_KEY=dev_api_key
SHOPIFY_API_SECRET=dev_api_secret
SHOPIFY_STORE=dev-store.myshopify.com
SHOPIFY_ACCESS_TOKEN=shpat_dev_token
SHOPIFY_APP_URL=https://localhost:3000
SHOPIFY_API_VERSION=2024-10
NODE_ENV=development
# .env.staging (git-ignored)
SHOPIFY_API_KEY=staging_api_key
SHOPIFY_API_SECRET=staging_api_secret
SHOPIFY_STORE=staging-store.myshopify.com
SHOPIFY_ACCESS_TOKEN=shpat_staging_token
SHOPIFY_APP_URL=https://staging.your-app.com
SHOPIFY_API_VERSION=2024-10
NODE_ENV=staging
# .env.production (never on disk — use secret manager)
# All values stored in Vault / AWS Secrets Manager / GCP Secret Manager
Step 3: Shopify CLI Configuration Per Environment
# shopify.app.dev.toml — development config
name = "My App (Dev)"
client_id = "dev_api_key"
[access_scopes]
scopes = "read_products,write_products,read_orders,write_orders"
[auth]
redirect_urls = ["https://localhost/auth/callback"]
[webhooks]
api_version = "2024-10"
# Switch between app configs
shopify app config use shopify.app.dev.toml
shopify app dev
shopify app config use shopify.app.toml # production
shopify app deploy
Step 4: Environment-Aware Configuration
// src/config.ts
interface ShopifyEnvConfig {
apiKey: string;
apiSecret: string;
appUrl: string;
apiVersion: string;
scopes: string[];
environment: "development" | "staging" | "production";
debug: boolean;
sessionStorageType: "memory" | "sqlite" | "postgresql";
}
function getCSet up observability for Shopify app integrations with query cost tracking, rate limit monitoring, webhook delivery metrics, and structured logging.
Shopify Observability
Overview
Instrument your Shopify app to track GraphQL query cost, rate limit consumption, webhook delivery success, and API latency. Shopify-specific metrics that generic monitoring misses.
Prerequisites
- Prometheus or compatible metrics backend
- pino or similar structured logger
- Shopify API client with response interception
Instructions
Step 1: Shopify-Specific Metrics
import { Registry, Counter, Histogram, Gauge } from "prom-client";
const registry = new Registry();
// GraphQL query cost tracking
const queryCostHistogram = new Histogram({
name: "shopify_graphql_query_cost",
help: "Shopify GraphQL actual query cost",
labelNames: ["operation", "shop"],
buckets: [1, 10, 50, 100, 250, 500, 1000],
registers: [registry],
});
// Rate limit headroom
const rateLimitGauge = new Gauge({
name: "shopify_rate_limit_available",
help: "Shopify rate limit points currently available",
labelNames: ["shop", "api_type"],
registers: [registry],
});
// REST bucket state
const restBucketGauge = new Gauge({
name: "shopify_rest_bucket_used",
help: "REST API leaky bucket current fill level",
labelNames: ["shop"],
registers: [registry],
});
// API request duration
const apiDuration = new Histogram({
name: "shopify_api_duration_seconds",
help: "Shopify API call duration",
labelNames: ["operation", "status", "api_type"],
buckets: [0.1, 0.25, 0.5, 1, 2, 5, 10],
registers: [registry],
});
// Webhook processing
const webhookCounter = new Counter({
name: "shopify_webhooks_total",
help: "Shopify webhooks received",
labelNames: ["topic", "status"], // status: success, error, invalid_hmac
registers: [registry],
});
// API errors by type
const apiErrors = new Counter({
name: "shopify_api_errors_total",
help: "Shopify API errors by type",
labelNames: ["error_type", "status_code"],
registers: [registry],
});
Step 2: Instrumented GraphQL Client
async function instrumentedGraphqlQuery<T>(
shop: string,
operation: string,
query: string,
variables?: Record<string, unknown>
): Promise<T> {
const timer = apiDuration.startTimer({ operation, api_type: "graphql" });
try {
const client = getGraphqlClient(shop);
const response = await client.request(query, { variables });
// Record cost metrics from Shopify's response
const cost = response.extensions?.cost;
if (cost) {
queryCostHistogram.observe(
{ operation, shop },
cost.actualQueryCost || cost.requestedQueryCost
);
rateLimitGauge.set(
Optimize Shopify API performance with GraphQL query cost reduction, bulk operations, caching strategies, and Storefront API for high-traffic storefronts.
Shopify Performance Tuning
Overview
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.
Prerequisites
- Understanding of Shopify's calculated query cost system
- Access to the
Shopify-GraphQL-Cost-Debug: 1header for cost analysis - Redis or in-memory cache available (optional)
Instructions
Step 1: Analyze Query Cost
# 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.
Step 2: Reduce Query Cost
// 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 }
}
}`;
Step 3: Use Bulk Operations for Large Exports
Bulk operations bypass rate limits and are designed for exporting large datasets:
Implement Shopify app policy enforcement with ESLint rules for API key detection, query cost budgets, and App Store compliance checks.
ReadWriteEditBash(npx:*)
Shopify Policy & Guardrails
Overview
Automated policy enforcement for Shopify apps: secret detection, query cost budgets, App Store compliance checks, and CI policy validation.
Prerequisites
- ESLint configured in project
- Pre-commit hooks infrastructure
- CI/CD pipeline with GitHub Actions
- Shopify app with
shopify.app.toml
Instructions
Step 1: Secret Detection Rules
// eslint-rules/no-shopify-secrets.js
module.exports = {
meta: {
type: "problem",
docs: { description: "Detect hardcoded Shopify tokens and secrets" },
messages: {
adminToken: "Hardcoded Shopify Admin API token detected (shpat_*)",
apiSecret: "Potential Shopify API secret detected",
storefrontToken: "Hardcoded Storefront API token detected",
},
},
create(context) {
return {
Literal(node) {
if (typeof node.value !== "string") return;
const v = node.value;
// Admin API access token: shpat_ + 32 hex chars
if (/^shpat_[a-f0-9]{32}$/i.test(v)) {
context.report({ node, messageId: "adminToken" });
}
// Storefront token: shpss_ pattern
if (/^shpss_[a-f0-9]{32}$/i.test(v)) {
context.report({ node, messageId: "storefrontToken" });
}
// Generic secret pattern (32+ hex that's clearly a token)
if (/^[a-f0-9]{32,}$/i.test(v) && v.length === 32) {
context.report({ node, messageId: "apiSecret" });
}
},
TemplateLiteral(node) {
for (const quasi of node.quasis) {
if (/shpat_[a-f0-9]/i.test(quasi.value.raw)) {
context.report({ node, messageId: "adminToken" });
}
}
},
};
},
};
Step 2: Query Cost Budget Enforcement
// Enforce query cost budgets at build/test time
interface QueryCostBudget {
maxFirstParam: number; // Max items per page
maxNestedDepth: number; // Max nested connection depth
maxEstimatedCost: number; // Max estimated query cost
}
const BUDGET: QueryCostBudget = {
maxFirstParam: 100, // Never request more than 100 items
maxNestedDepth: 3, // No more than 3 levels of edges/node
maxEstimatedCost: 500, // Stay well under 1,000 point limit
};
function validateQueryCost(query: string): string[] {
const violations: string[] = [];
// Check `first:` parameter values
const firstParams = query.matchAll(/first:\s*(\d+)/g);
for (const match of firstParams) {
if (parseInt(match[1]) > BUDGET.maxFirstParam) {
violations.push(
`first: ${match[1]} exceeds budget of ${BUDGET.maxFirstParam}`
);
}
}
// Check nesting depth (count "edges { node {" patterns)
Execute Shopify app production deployment checklist covering App Store requirements, mandatory webhooks, API versioning, and rollback procedures.
Shopify Production Checklist
Overview
Complete pre-launch checklist for deploying Shopify apps to production and submitting to the Shopify App Store.
Prerequisites
- Staging environment tested and verified
- Shopify Partner account with app configured
- All development and staging tests passing
Instructions
Step 1: API and Authentication
- [ ] Using a stable API version (e.g.,
2024-10), notunstable - [ ] Access token stored in secure environment variables (never in code)
- [ ] API secret stored securely for webhook HMAC verification
- [ ] OAuth flow tested with a fresh install on a clean dev store
- [ ] Session persistence implemented (database or Redis, not in-memory)
- [ ] Token refresh/re-auth handled for expired sessions
- [ ]
APP_UNINSTALLEDwebhook handler cleans up sessions
Step 2: Mandatory GDPR Compliance
- [ ]
customers/data_requestwebhook handler implemented - [ ]
customers/redactwebhook handler implemented - [ ]
shop/redactwebhook handler implemented (fires 48h after uninstall) - [ ] All three configured in
shopify.app.toml - [ ] Handlers respond with HTTP 200 within 5 seconds
- [ ] Customer data deletion actually works (test it!)
Step 3: Webhook Security
- [ ] All webhooks verify
X-Shopify-Hmac-Sha256using HMAC-SHA256 - [ ] Using
crypto.timingSafeEqual()for signature comparison - [ ] Webhook endpoints use raw body parsing (not JSON middleware)
- [ ] Idempotency: duplicate webhook deliveries handled gracefully
Step 4: Rate Limit Resilience
- [ ] GraphQL queries optimized (check
requestedQueryCostwith debug header) - [ ] Retry logic with exponential backoff for 429 / THROTTLED responses
- [ ] Bulk operations used for large data exports instead of paginated queries
- [ ] No unbounded loops that could exhaust rate limits
Step 5: Error Handling
- [ ] All GraphQL mutations check
userErrorsarray (200 with errors!) - [ ] HTTP 4xx/5xx errors caught and logged with
X-Request-Id - [ ] Graceful degradation when Shopify is unavailable
- [ ] No PII logged (customer emails, addresses, phone numbers)
Step 6: App Store Submission Requirements
- [ ] App listing has clear name, description, and screenshots
- [ ] Privacy policy URL provided
- [ ] App has proper onboarding flow for new merchants
- [ ] Embedded app uses App Bridge for navigation (no full-page redirects)
- [ ] CSP headers set:
frame-ancestors https://*.myshopify.com https://admin.shopify.com - [ ] App works on both desktop
Handle Shopify API rate limits for both REST (leaky bucket) and GraphQL (calculated query cost).
Shopify Rate Limits
Overview
Shopify uses two distinct rate limiting systems: leaky bucket for REST and calculated query cost for GraphQL. This skill covers both with real header values and response shapes.
Prerequisites
- Understanding of Shopify's REST and GraphQL Admin APIs
- Familiarity with the
@shopify/shopify-apilibrary
Instructions
Step 1: Understand the Two Rate Limit Systems
REST Admin API — Leaky Bucket:
| Plan | Bucket Size | Leak Rate |
|---|---|---|
| Standard | 40 requests | 2/second |
| Shopify Plus | 80 requests | 4/second |
The X-Shopify-Shop-Api-Call-Limit header shows your bucket state:
X-Shopify-Shop-Api-Call-Limit: 32/40
Means: 32 of 40 slots used. When full, you get HTTP 429 with Retry-After header.
GraphQL Admin API — Calculated Query Cost:
| Plan | Max Available | Restore Rate |
|---|---|---|
| Standard | 1,000 points | 50 points/second |
| Shopify Plus | 2,000 points | 100 points/second |
Every GraphQL response includes cost info in extensions:
{
"extensions": {
"cost": {
"requestedQueryCost": 252,
"actualQueryCost": 12,
"throttleStatus": {
"maximumAvailable": 1000.0,
"currentlyAvailable": 988.0,
"restoreRate": 50.0
}
}
}
}
Key insight: requestedQueryCost is the worst case estimate. actualQueryCost is the real cost (often much lower). When currentlyAvailable drops to 0, you get THROTTLED.
Step 2: Implement GraphQL Cost-Aware Throttling
interface ShopifyThrottleStatus {
maximumAvailable: number;
currentlyAvailable: number;
restoreRate: number;
}
class ShopifyRateLimiter {
private available: number;
private restoreRate: number;
private lastUpdate: number;
constructor(maxAvailable = 1000, restoreRate = 50) {
this.available = maxAvailable;
this.restoreRate = restoreRate;
this.lastUpdate = Date.now();
}
updateFromResponse(throttleStatus: ShopifyThrottleStatus): void {
this.available = throttleStatus.currentlyAvailable;
this.restoreRate = throttleStatus.restoreRate;
this.lastUpdate = Date.now();
}
async waitIfNeeded(estimatedCost: number): Promise<void> {
// Estimate current available based on restore rate
const elapsed = (Date.now() - this.lastUpdate) / 1000;
const estimated = Math.min(
this.available + Implement Shopify app reference architecture with Remix, Prisma session storage, and the official app template patterns.
Shopify Reference Architecture
Overview
Production-ready architecture based on Shopify's official Remix app template. Covers project structure, session storage with Prisma, extension architecture, and the recommended app patterns.
Prerequisites
- Understanding of Remix framework basics
- Shopify CLI 3.x installed
- Familiarity with Prisma ORM
Instructions
Step 1: Official Project Structure (Remix Template)
my-shopify-app/
├── app/
│ ├── routes/
│ │ ├── app._index.tsx # Main app dashboard
│ │ ├── app.products.tsx # Product management page
│ │ ├── app.settings.tsx # App settings
│ │ ├── auth.$.tsx # OAuth catch-all route
│ │ ├── auth.login/
│ │ │ └── route.tsx # Login page
│ │ └── webhooks.tsx # Webhook handler
│ ├── shopify.server.ts # Shopify API config (singleton)
│ ├── db.server.ts # Database connection
│ └── root.tsx
├── extensions/
│ ├── theme-app-extension/ # Theme blocks for Online Store
│ │ ├── blocks/
│ │ │ └── product-rating.liquid
│ │ └── locales/
│ ├── checkout-ui/ # Checkout UI extension
│ └── product-discount/ # Shopify Function
├── prisma/
│ ├── schema.prisma # Database schema
│ └── migrations/
├── shopify.app.toml # App configuration
├── shopify.web.toml # Web process config
├── remix.config.js
└── package.json
Step 2: Core App Configuration
// app/shopify.server.ts — the heart of the app
import "@shopify/shopify-app-remix/adapters/node";
import { AppDistribution, shopifyApp } from "@shopify/shopify-app-remix/server";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
appUrl: process.env.SHOPIFY_APP_URL!,
scopes: process.env.SHOPIFY_SCOPES?.split(","),
apiVersion: "2024-10",
distribution: AppDistribution.AppStore, // or SingleMerchant
sessionStorage: new PrismaSessionStorage(prisma),
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: "http",
callbackUrl: "/webhooks",
},
PRODUCTS_UPDATE: {
deliveryMethod: "http",
callbackUrl: "/webhooks",
},
},
hooks: {
afterAuth: async ({ session }) => {
// Register webhooks after successful auth
shopify.registerWebhooks({ session });
},
},
future: {
unstable_newEmbeddedAuthStrategy: true,
},
});
export default shopify;
export const apiVersion = "2024-10";
export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
export const authenticatImplement reliability patterns for Shopify apps including circuit breakers for API outages, webhook retry handling, and graceful degradation.
Shopify Reliability Patterns
Overview
Build fault-tolerant Shopify integrations that handle API outages, webhook retry storms, and rate limit exhaustion gracefully.
Prerequisites
- Understanding of circuit breaker pattern
- Queue infrastructure (BullMQ, SQS, etc.) for async processing
- Cache layer for fallback data
Instructions
Step 1: Circuit Breaker for Shopify API
import CircuitBreaker from "opossum";
// Create circuit breaker wrapping Shopify API calls
const shopifyCircuit = new CircuitBreaker(
async (fn: () => Promise<any>) => fn(),
{
timeout: 10000, // 10s timeout per request
errorThresholdPercentage: 50, // Open at 50% error rate
resetTimeout: 30000, // Try half-open after 30s
volumeThreshold: 5, // Need 5 requests before tripping
errorFilter: (error: any) => {
// Don't count 422 validation errors as circuit failures
// Only count 5xx and timeout errors
const code = error.response?.code || error.statusCode;
return code >= 500 || error.code === "ECONNRESET" || error.code === "ETIMEDOUT";
},
}
);
shopifyCircuit.on("open", () => {
console.error("[CIRCUIT OPEN] Shopify API failing — serving cached data");
});
shopifyCircuit.on("halfOpen", () => {
console.info("[CIRCUIT HALF-OPEN] Testing Shopify recovery...");
});
shopifyCircuit.on("close", () => {
console.info("[CIRCUIT CLOSED] Shopify API recovered");
});
// Usage
async function resilientShopifyQuery<T>(
shop: string,
query: string,
variables?: Record<string, unknown>
): Promise<T> {
return shopifyCircuit.fire(async () => {
const client = getGraphqlClient(shop);
const response = await client.request(query, { variables });
// Check for THROTTLED in GraphQL response
if (response.errors?.some((e: any) => e.extensions?.code === "THROTTLED")) {
throw new Error("THROTTLED"); // Triggers circuit breaker
}
return response.data as T;
});
}
Step 2: Webhook Idempotency
Shopify retries webhooks up to 19 times over 48 hours if your endpoint doesn't return 200. Your handler must be idempotent.
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);
async function processWebhookIdempotently(
webhookId: string, // X-Shopify-Webhook-Id header
topic: string,
handler: () => Promise<void>
): Promise<{ processed: boolean; duplicate: boolean }> {
const key = `shopify:webhook:${webhookId}`;
// Check if already processed
const exists = await redis.exists(key);
if (exists) {
console.log(`Duplicate webhook ${webhookId} for ${topic} — skipping`)Apply production-ready patterns for @shopify/shopify-api including typed GraphQL clients, session management, and retry logic.
Shopify SDK Patterns
Overview
Production-ready patterns for the @shopify/shopify-api library: singleton clients, typed GraphQL operations, session management, cursor-based pagination, and error handling wrappers.
Prerequisites
@shopify/shopify-apiv9+ installed- Familiarity with Shopify's GraphQL Admin API
- Understanding of async/await and TypeScript generics
Instructions
Step 1: Typed GraphQL Client Wrapper
// src/shopify/client.ts
import "@shopify/shopify-api/adapters/node";
import { shopifyApi, Session, GraphqlClient } from "@shopify/shopify-api";
const shopify = shopifyApi({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
hostName: process.env.SHOPIFY_HOST_NAME!,
apiVersion: "2024-10",
isCustomStoreApp: !!process.env.SHOPIFY_ACCESS_TOKEN,
adminApiAccessToken: process.env.SHOPIFY_ACCESS_TOKEN,
});
// Singleton session cache per shop
const sessionCache = new Map<string, Session>();
export function getSession(shop: string): Session {
if (!sessionCache.has(shop)) {
const session = shopify.session.customAppSession(shop);
sessionCache.set(shop, session);
}
return sessionCache.get(shop)!;
}
export function getGraphqlClient(shop: string): GraphqlClient {
return new shopify.clients.Graphql({
session: getSession(shop),
});
}
// Typed query helper
export async function shopifyQuery<T = any>(
shop: string,
query: string,
variables?: Record<string, unknown>
): Promise<T> {
const client = getGraphqlClient(shop);
const response = await client.request(query, { variables });
return response.data as T;
}
Step 2: Error Handling with Shopify Error Types
// src/shopify/errors.ts
import { HttpResponseError, GraphqlQueryError } from "@shopify/shopify-api";
export class ShopifyServiceError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly retryable: boolean,
public readonly shopifyRequestId?: string,
public readonly originalError?: Error
) {
super(message);
this.name = "ShopifyServiceError";
}
}
export function handleShopifyError(error: unknown): never {
if (error instanceof HttpResponseError) {
const retryable = [429, 500, 502, 503, 504].includes(error.response.code);
throw new ShopifyServiceError(
`Shopify API ${error.response.code}: ${error.message}`,
error.response.code,
retryable,
error.response.headers?.["x-request-id"] as string,
error
);
}
if (error instanceof GraphqlQueryError) {
// GraphQL errors in the response body
const msg = error.body?.errors
?.map((e: any) => e.message)
.join("; ") || error.message;
throw new ShApply Shopify security best practices for API credentials, webhook HMAC validation, and access scope management.
Shopify Security Basics
Overview
Security essentials for Shopify apps: credential management, webhook HMAC validation, request verification, and least-privilege access scopes.
Prerequisites
- Shopify Partner account with app credentials
- Understanding of HMAC-SHA256 signatures
- Access to Shopify app configuration
Instructions
Step 1: Secure Credential Storage
# .env — NEVER commit
SHOPIFY_API_KEY=your_api_key
SHOPIFY_API_SECRET=your_api_secret_key
SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# .gitignore — add immediately
.env
.env.local
.env.*.local
*.pem
Token format reference:
| Token Type | Prefix | Length | Used For |
|---|---|---|---|
| Admin API access token | shpat_ |
38 chars | Server-side Admin API |
| Storefront API token | varies | varies | Client-safe storefront queries |
| API secret key | none | 32+ hex | Webhook HMAC, OAuth |
Step 2: Webhook HMAC Verification
Shopify signs every webhook with your app's API secret using HMAC-SHA256. The signature is in the X-Shopify-Hmac-Sha256 header.
import crypto from "crypto";
import express from "express";
function verifyShopifyWebhook(
rawBody: Buffer,
hmacHeader: string,
secret: string
): boolean {
const computed = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("base64");
// Timing-safe comparison prevents timing attacks
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(hmacHeader)
);
}
// Express middleware — MUST use raw body parser
app.post(
"/webhooks",
express.raw({ type: "application/json" }),
(req, res) => {
const hmac = req.headers["x-shopify-hmac-sha256"] as string;
const topic = req.headers["x-shopify-topic"] as string;
const shop = req.headers["x-shopify-shop-domain"] as string;
if (!verifyShopifyWebhook(req.body, hmac, process.env.SHOPIFY_API_SECRET!)) {
console.warn(`Invalid webhook HMAC from ${shop}, topic: ${topic}`);
return res.status(401).send("HMAC validation failed");
}
const payload = JSON.parse(req.body.toString());
console.log(`Verified webhook: ${topic} from ${shop}`);
// Process asynchronously — respond 200 within 5 seconds
processWebhookAsync(topic, shop, payload);
res.status(200).send("OK");
}
);
Step 3: OAuth Request Verification
Verify that incoming requests from Shopify are authentic by checking the HMAC query parameter:
import { shopifyApi } froUpgrade Shopify API versions and migrate from REST to GraphQL with breaking change detection.
Shopify Upgrade & Migration
Overview
Guide for upgrading Shopify API versions (quarterly releases) and migrating from the legacy REST Admin API to the GraphQL Admin API. REST was deprecated as a legacy API on October 1, 2024.
Prerequisites
- Current Shopify API version identified
- Git for version control
- Test suite available
- Access to Shopify release notes
Instructions
Step 1: Check Current Version and Available Versions
# Check what API version you're using in code
grep -r "apiVersion" src/ --include="*.ts" --include="*.js"
grep -r "api_version" . --include="*.toml"
# Check what versions the store supports
curl -s -H "X-Shopify-Access-Token: $TOKEN" \
"https://$STORE/admin/api/versions.json" \
| jq '.supported_versions[] | {handle, display_name, supported, latest}'
Shopify releases quarterly: 2024-01, 2024-04, 2024-07, 2024-10. Versions are supported for ~12 months after release.
Step 2: Review Breaking Changes
Key breaking changes by version:
| Version | Breaking Change | Migration |
|---|---|---|
| 2024-10 | ProductInput split into ProductCreateInput + ProductUpdateInput |
Update mutations to use separate types |
| 2024-10 | REST declared legacy | Migrate to GraphQL Admin API |
| 2024-07 | InventoryItem.unitCost removed |
Use InventoryItem.unitCost on InventoryLevel |
| 2024-04 | Cart warnings replace inventory userErrors (Storefront) | Update cart error handling |
| 2025-01 | New public apps must use GraphQL only | No REST for new public apps |
Step 3: Migrate REST to GraphQL
// BEFORE: REST Admin API
const restClient = new shopify.clients.Rest({ session });
const { body } = await restClient.get({
path: "products",
query: { limit: 50, status: "active" },
});
const products = body.products;
// AFTER: GraphQL Admin API
const graphqlClient = new shopify.clients.Graphql({ session });
const response = await graphqlClient.request(`{
products(first: 50, query: "status:active") {
edges {
node {
id
title
status
variants(first: 10) {
edges { node { id price sku } }
}
}
}
pageInfo { hasNextPage endCursor }
}
}`);
const products = response.data.products.edges.map((e: any) => e.node);
Common REST-to-GraphQL mappings:
| REST Endpoint | GraphQL Query/Mutation |
|---|