Claude-code-plugins shopify-sdk-patterns

install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/shopify-pack/skills/shopify-sdk-patterns" ~/.claude/skills/jeremylongshore-claude-code-plugins-shopify-sdk-patterns && rm -rf "$T"
manifest: plugins/saas-packs/shopify-pack/skills/shopify-sdk-patterns/SKILL.md
source content

Shopify SDK Patterns

Overview

Production-ready patterns for the

@shopify/shopify-api
library: singleton clients, typed GraphQL operations, session management, cursor-based pagination, codegen-typed operations, bulk operations, and webhook registry patterns.

Prerequisites

  • @shopify/shopify-api
    v9+ installed
  • Familiarity with Shopify's GraphQL Admin API
  • Understanding of async/await and TypeScript generics

Instructions

Step 1: Typed GraphQL Client Wrapper

Initialize a singleton

shopifyApi
instance with
LATEST_API_VERSION
, cache sessions per shop, and expose a typed
shopifyQuery<T>()
helper that wraps
client.request()
.

See Typed GraphQL Client for the complete implementation.

Step 2: Error Handling with Shopify Error Types

Custom

ShopifyServiceError
class that distinguishes retryable errors (429, 5xx) from permanent ones. Includes
handleShopifyError()
for error translation and
safeShopifyCall()
that returns
{data, error}
tuples instead of throwing.

See Error Handling for the complete implementation.

Step 3: Cursor-Based Pagination

Async generator

paginateShopify<T>()
for Relay-style cursor pagination. Yields batches of nodes, automatically following
pageInfo.endCursor
until
hasNextPage
is false. Memory-efficient for large datasets.

See Cursor Pagination for the complete implementation.

Step 4: Multi-Tenant Client Factory

ShopifyClientFactory
class for apps installed on multiple stores. Creates isolated
GraphqlClient
instances per merchant with session caching. Includes
removeClient()
for eviction on app uninstall.

See Multi-Tenant Factory for the complete implementation.

Step 5: Codegen-Typed Operations

Use

@shopify/api-codegen-preset
to generate TypeScript types from your GraphQL operations. This eliminates manual type definitions and catches schema changes at build time.

// codegen.ts — project root config
import { shopifyApiProject, ApiType } from "@shopify/api-codegen-preset";

export default {
  schema: "https://shopify.dev/admin-graphql-direct-proxy",
  documents: ["src/**/*.{ts,tsx}"],
  projects: {
    default: shopifyApiProject({
      apiType: ApiType.Admin,
      apiVersion: "2025-04", // Update quarterly
      outputDir: "./src/types",
    }),
  },
};
// src/operations/products.ts — typed query with codegen output
import type { ProductsQuery } from "../types/admin.generated";

const PRODUCTS_QUERY = `#graphql
  query Products($first: Int!, $after: String) {
    products(first: $first, after: $after) {
      edges {
        node { id title status totalInventory }
      }
      pageInfo { hasNextPage endCursor }
    }
  }
` as const;

// Return type is fully inferred from codegen
export async function getProducts(shop: string): Promise<ProductsQuery> {
  return shopifyQuery<ProductsQuery>(shop, PRODUCTS_QUERY, { first: 50 });
}

Run

npx graphql-codegen
after changing any GraphQL operation or upgrading API versions.

Step 6: Bulk Operation Helpers

For datasets too large for pagination (100k+ records), use Shopify's Bulk Operations API. It runs a query server-side and produces a JSONL file you download when ready.

// src/shopify/bulk.ts
const BULK_QUERY = `
  mutation bulkOperationRunQuery($query: String!) {
    bulkOperationRunQuery(query: $query) {
      bulkOperation { id status }
      userErrors { field message }
    }
  }
`;

const POLL_QUERY = `{
  currentBulkOperation {
    id status errorCode objectCount url
  }
}`;

export async function runBulkOperation(
  shop: string,
  query: string
): Promise<string> {
  // Start the bulk operation
  const { bulkOperationRunQuery } = await shopifyQuery(shop, BULK_QUERY, { query });
  if (bulkOperationRunQuery.userErrors?.length) {
    throw new Error(bulkOperationRunQuery.userErrors[0].message);
  }

  // Poll until complete (typically 1-10 minutes for large datasets)
  let result;
  do {
    await new Promise((r) => setTimeout(r, 5000)); // 5s interval
    result = (await shopifyQuery(shop, POLL_QUERY)).currentBulkOperation;
  } while (result.status === "RUNNING" || result.status === "CREATED");

  if (result.status !== "COMPLETED") {
    throw new Error(`Bulk operation failed: ${result.errorCode}`);
  }

  return result.url; // JSONL download URL
}

// Usage: export all products
const url = await runBulkOperation(shop, `{
  products { edges { node { id title status variants { edges { node { sku price } } } } } }
}`);
const response = await fetch(url);
const jsonl = await response.text();
const products = jsonl.trim().split("\n").map(JSON.parse);

Step 7: Webhook Registry Patterns

Programmatically register webhook subscriptions using

webhookSubscriptionCreate
with typed
WebhookSubscriptionInput
. Supports both HTTP and EventBridge/PubSub endpoints.

// src/shopify/webhooks.ts
const REGISTER_WEBHOOK = `
  mutation webhookSubscriptionCreate(
    $topic: WebhookSubscriptionTopic!,
    $webhookSubscription: WebhookSubscriptionInput!
  ) {
    webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
      webhookSubscription { id topic }
      userErrors { field message }
    }
  }
`;

interface WebhookConfig {
  topic: string;
  callbackUrl: string;
  format?: "JSON" | "XML";
}

export async function registerWebhooks(
  shop: string,
  webhooks: WebhookConfig[]
): Promise<{ registered: string[]; errors: string[] }> {
  const registered: string[] = [];
  const errors: string[] = [];

  for (const wh of webhooks) {
    const result = await shopifyQuery(shop, REGISTER_WEBHOOK, {
      topic: wh.topic,
      webhookSubscription: {
        callbackUrl: wh.callbackUrl,
        format: wh.format ?? "JSON",
      },
    });

    const userErrors = result.webhookSubscriptionCreate.userErrors;
    if (userErrors?.length) {
      errors.push(`${wh.topic}: ${userErrors[0].message}`);
    } else {
      registered.push(wh.topic);
    }
  }

  return { registered, errors };
}

Output

  • Type-safe GraphQL client with singleton session management
  • Structured error handling that distinguishes retryable from permanent errors
  • Cursor-based pagination generator for large datasets
  • Multi-tenant client factory for apps serving multiple stores
  • Codegen-typed operations eliminating manual type definitions
  • Bulk operation helpers for large dataset exports
  • Webhook registry patterns for programmatic subscription management

Error Handling

PatternUse CaseBenefit
safeShopifyCall
All API callsReturns
{data, error}
instead of throwing
handleShopifyError
Error translationMaps HTTP/GraphQL errors to typed errors
Cursor paginationLarge datasetsMemory-efficient streaming with backpressure
Bulk operations100k+ recordsServer-side execution, no client memory pressure
Client factoryMulti-tenant appsIsolated sessions per merchant

Examples

Setting Up a Type-Safe GraphQL Client

Initialize a singleton Shopify client with session caching and a typed

shopifyQuery<T>()
helper for all API calls.

See Typed GraphQL Client for the complete implementation.

Handling Retryable vs Permanent Errors

Distinguish 429/5xx retryable errors from permanent validation failures using a structured error class and safe call wrapper.

See Error Handling for the complete error handling implementation.

Building a Multi-Tenant App

Create isolated GraphQL clients per merchant with session caching and eviction on app uninstall using a client factory.

See Multi-Tenant Factory for the complete implementation.

Resources