All skills
jeffallan avatar

/shopify-expert

@efebc44
by jeffallanjeffallan/claude-skills12k stars
1,124

Builds and debugs Shopify themes (.liquid files, theme.json, sections), develops custom Shopify apps (shopify.app.toml, OAuth, webhooks), and implements Storefront API integrations for headless storefronts. Use when building or customizing Shopify themes, creating Hydrogen or custom React storefronts, developing Shopify apps, implementing checkout UI extensions or Shopify Functions, optimizing performance, or integrating third-party services. Invoke for Liquid templating, Storefront API, app development, checkout customization, Shopify Plus features, App Bridge, Polaris, or Shopify CLI workflows.

Use this Skill: https://skilld.dev/gh/jeffallan/claude-skills/shopify-expert

This session only. Nothing lands on disk.

referencesapp-development.md

≈5.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

App Development


When to Use

  • Building custom Shopify apps for merchants
  • Creating public apps for the Shopify App Store
  • Integrating third-party services with Shopify
  • Automating merchant workflows
  • Building embedded admin experiences

When NOT to Use

  • Theme customization (use Liquid)
  • Customer-facing storefronts (use Storefront API)
  • Simple product displays (use Liquid or Storefront API)
  • Checkout-only customizations (use Checkout Extensions)

App Architecture Overview

App Types

Type Use Case Distribution
Custom App Single merchant, private Manual install
Public App App Store listing Shopify review
Sales Channel Custom storefront App Store
Embedded App Admin integration Either

Modern Stack (2024+)

# Create new Shopify app with Remix template
npm create @shopify/app@latest

# Project structure
shopify-app/
├── app/
│   ├── routes/              # Remix routes
│   │   ├── app._index.tsx   # Main app page
│   │   ├── app.products.tsx # Products page
│   │   └── webhooks.tsx     # Webhook handlers
│   ├── shopify.server.ts    # Shopify API client
│   └── db.server.ts         # Database client
├── extensions/              # App extensions
├── prisma/                  # Database schema
├── shopify.app.toml         # App configuration
└── package.json

App Configuration

shopify.app.toml

# shopify.app.toml
scopes = "read_products,write_products,read_orders,write_orders,read_customers"

[access_scopes]
# Use optional scopes for granular permissions
optional = ["read_inventory", "write_inventory"]

[auth]
redirect_urls = [
  "https://your-app.com/auth/callback",
  "https://your-app.com/auth/shopify/callback"
]

[webhooks]
api_version = "2024-10"

  [[webhooks.subscriptions]]
  topics = ["products/create", "products/update", "products/delete"]
  uri = "/webhooks"

  [[webhooks.subscriptions]]
  topics = ["orders/create"]
  uri = "/webhooks"

  [[webhooks.subscriptions]]
  topics = ["app/uninstalled"]
  uri = "/webhooks"

[app_proxy]
url = "https://your-app.com/api/proxy"
subpath = "apps"
prefix = "your-app"

[pos]
embedded = false

[build]
automatically_update_urls_on_dev = true
dev_store_url = "your-dev-store.myshopify.com"

[app]
name = "Your App Name"
handle = "your-app-handle"

OAuth Implementation

Authentication Flow

// app/shopify.server.ts
import "@shopify/shopify-app-remix/adapters/node";
import {
  ApiVersion,
  AppDistribution,
  shopifyApp,
  DeliveryMethod,
} 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.SCOPES?.split(","),
  apiVersion: ApiVersion.October24,
  distribution: AppDistribution.AppStore,
  sessionStorage: new PrismaSessionStorage(prisma),
  webhooks: {
    APP_UNINSTALLED: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks",
    },
    PRODUCTS_CREATE: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks",
    },
    ORDERS_CREATE: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks",
    },
  },
  hooks: {
    afterAuth: async ({ session, admin }) => {
      // Register webhooks after OAuth
      shopify.registerWebhooks({ session });

      // Perform post-install setup
      await setupShop(session, admin);
    },
  },
});

async function setupShop(session: Session, admin: AdminApiContext) {
  // Store merchant data
  await prisma.shop.upsert({
    where: { shopDomain: session.shop },
    update: { accessToken: session.accessToken },
    create: {
      shopDomain: session.shop,
      accessToken: session.accessToken!,
      installedAt: new Date(),
    },
  });
}

export default shopify;
export const apiVersion = ApiVersion.October24;
export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
export const authenticate = shopify.authenticate;
export const unauthenticated = shopify.unauthenticated;
export const login = shopify.login;
export const registerWebhooks = shopify.registerWebhooks;
export const sessionStorage = shopify.sessionStorage;

Protected Routes

// app/routes/app._index.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { Page, Layout, Card, DataTable } from "@shopify/polaris";
import { authenticate } from "../shopify.server";

export async function loader({ request }: LoaderFunctionArgs) {
  const { admin, session } = await authenticate.admin(request);

  // Make Admin API requests
  const response = await admin.graphql(`
    query {
      shop {
        name
        email
        myshopifyDomain
        plan {
          displayName
        }
      }
      products(first: 10) {
        edges {
          node {
            id
            title
            status
            totalInventory
          }
        }
      }
    }
  `);

  const data = await response.json();

  return json({
    shop: data.data.shop,
    products: data.data.products.edges.map((edge: any) => edge.node),
  });
}

export default function Index() {
  const { shop, products } = useLoaderData<typeof loader>();

  const rows = products.map((product: any) => [
    product.title,
    product.status,
    product.totalInventory,
  ]);

  return (
    <Page title={`Welcome to ${shop.name}`}>
      <Layout>
        <Layout.Section>
          <Card>
            <DataTable
              columnContentTypes={["text", "text", "numeric"]}
              headings={["Product", "Status", "Inventory"]}
              rows={rows}
            />
          </Card>
        </Layout.Section>
      </Layout>
    </Page>
  );
}

Admin API (GraphQL)

Products CRUD

// Create product
const CREATE_PRODUCT = `
  mutation productCreate($input: ProductInput!) {
    productCreate(input: $input) {
      product {
        id
        title
        handle
        variants(first: 10) {
          edges {
            node {
              id
              price
              sku
            }
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;

// Usage
const response = await admin.graphql(CREATE_PRODUCT, {
  variables: {
    input: {
      title: "New Product",
      descriptionHtml: "<p>Product description</p>",
      vendor: "Your Brand",
      productType: "T-Shirt",
      tags: ["new", "featured"],
      variants: [
        {
          price: "29.99",
          sku: "SKU-001",
          inventoryManagement: "SHOPIFY",
          inventoryPolicy: "DENY",
          options: ["Small", "Blue"],
        },
        {
          price: "29.99",
          sku: "SKU-002",
          options: ["Medium", "Blue"],
        },
      ],
      options: ["Size", "Color"],
    },
  },
});

// Update product
const UPDATE_PRODUCT = `
  mutation productUpdate($input: ProductInput!) {
    productUpdate(input: $input) {
      product {
        id
        title
      }
      userErrors {
        field
        message
      }
    }
  }
`;

// Bulk operations for large datasets
const BULK_MUTATION = `
  mutation bulkOperationRunMutation($mutation: String!, $stagedUploadPath: String!) {
    bulkOperationRunMutation(mutation: $mutation, stagedUploadPath: $stagedUploadPath) {
      bulkOperation {
        id
        status
      }
      userErrors {
        field
        message
      }
    }
  }
`;

Orders Management

// Fetch orders with fulfillment status
const GET_ORDERS = `
  query getOrders($first: Int!, $query: String) {
    orders(first: $first, query: $query, sortKey: CREATED_AT, reverse: true) {
      edges {
        node {
          id
          name
          createdAt
          displayFinancialStatus
          displayFulfillmentStatus
          totalPriceSet {
            shopMoney {
              amount
              currencyCode
            }
          }
          customer {
            firstName
            lastName
            email
          }
          lineItems(first: 5) {
            edges {
              node {
                title
                quantity
                variant {
                  id
                  sku
                }
              }
            }
          }
          shippingAddress {
            address1
            city
            province
            country
            zip
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

// Create fulfillment
const CREATE_FULFILLMENT = `
  mutation fulfillmentCreate($fulfillment: FulfillmentInput!) {
    fulfillmentCreate(fulfillment: $fulfillment) {
      fulfillment {
        id
        status
        trackingInfo {
          number
          url
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;

Metafields

// Set product metafields
const SET_METAFIELDS = `
  mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
    metafieldsSet(metafields: $metafields) {
      metafields {
        id
        namespace
        key
        value
        type
      }
      userErrors {
        field
        message
      }
    }
  }
`;

// Usage
await admin.graphql(SET_METAFIELDS, {
  variables: {
    metafields: [
      {
        ownerId: "gid://shopify/Product/123456789",
        namespace: "custom",
        key: "care_instructions",
        value: "Machine wash cold",
        type: "single_line_text_field",
      },
      {
        ownerId: "gid://shopify/Product/123456789",
        namespace: "custom",
        key: "features",
        value: JSON.stringify(["Organic cotton", "Fair trade", "Eco-friendly"]),
        type: "list.single_line_text_field",
      },
    ],
  },
});

// Read metafields
const GET_PRODUCT_METAFIELDS = `
  query getProductMetafields($id: ID!) {
    product(id: $id) {
      metafields(first: 20) {
        edges {
          node {
            id
            namespace
            key
            value
            type
          }
        }
      }
    }
  }
`;

Webhook Handling

Webhook Route

// app/routes/webhooks.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import db from "../db.server";

export async function action({ request }: ActionFunctionArgs) {
  const { topic, shop, session, admin, payload } =
    await authenticate.webhook(request);

  console.log(`Received ${topic} webhook for ${shop}`);

  switch (topic) {
    case "APP_UNINSTALLED":
      await handleAppUninstalled(shop);
      break;

    case "PRODUCTS_CREATE":
      await handleProductCreate(shop, payload);
      break;

    case "PRODUCTS_UPDATE":
      await handleProductUpdate(shop, payload);
      break;

    case "ORDERS_CREATE":
      await handleOrderCreate(shop, payload, admin);
      break;

    case "CUSTOMERS_DATA_REQUEST":
      await handleDataRequest(shop, payload);
      break;

    case "CUSTOMERS_REDACT":
      await handleCustomerRedact(shop, payload);
      break;

    case "SHOP_REDACT":
      await handleShopRedact(shop, payload);
      break;

    default:
      console.log(`Unhandled webhook topic: ${topic}`);
  }

  return new Response("OK", { status: 200 });
}

async function handleAppUninstalled(shop: string) {
  // Clean up shop data
  await db.shop.delete({
    where: { shopDomain: shop },
  });
}

async function handleProductCreate(shop: string, payload: any) {
  // Sync product to your database
  await db.product.create({
    data: {
      shopDomain: shop,
      shopifyId: payload.admin_graphql_api_id,
      title: payload.title,
      handle: payload.handle,
      status: payload.status,
    },
  });
}

async function handleOrderCreate(shop: string, payload: any, admin: any) {
  // Process new order
  const order = {
    id: payload.admin_graphql_api_id,
    orderNumber: payload.order_number,
    totalPrice: payload.total_price,
    customer: payload.customer,
    lineItems: payload.line_items,
  };

  // Example: Add order note
  await admin.graphql(`
    mutation addOrderNote($id: ID!, $note: String!) {
      orderUpdate(input: { id: $id, note: $note }) {
        order { id }
        userErrors { field message }
      }
    }
  `, {
    variables: {
      id: order.id,
      note: "Processed by Your App",
    },
  });
}

// GDPR webhooks (required for public apps)
async function handleDataRequest(shop: string, payload: any) {
  // Return customer data
  const customerId = payload.customer.id;
  // Gather and return all customer data
}

async function handleCustomerRedact(shop: string, payload: any) {
  // Delete customer data
  const customerId = payload.customer.id;
  await db.customerData.deleteMany({
    where: { shopDomain: shop, customerId: String(customerId) },
  });
}

async function handleShopRedact(shop: string, payload: any) {
  // Delete all shop data (48 hours after uninstall)
  await db.shop.delete({ where: { shopDomain: shop } });
}

App Bridge 4.0

Setup

// app/root.tsx
import { AppProvider } from "@shopify/shopify-app-remix/react";
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";

export const links = () => [{ rel: "stylesheet", href: polarisStyles }];

export default function App() {
  const { apiKey } = useLoaderData<typeof loader>();

  return (
    <html>
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <AppProvider isEmbeddedApp apiKey={apiKey}>
          <Outlet />
        </AppProvider>
        <Scripts />
      </body>
    </html>
  );
}

App Bridge Actions

// Using App Bridge in components
import { useAppBridge } from "@shopify/app-bridge-react";
import { Redirect } from "@shopify/app-bridge/actions";

function MyComponent() {
  const app = useAppBridge();

  const redirectToProduct = (productId: string) => {
    const redirect = Redirect.create(app);
    redirect.dispatch(Redirect.Action.ADMIN_PATH, {
      path: `/products/${productId}`,
    });
  };

  const openResourcePicker = async () => {
    const selection = await app.resourcePicker({
      type: "product",
      multiple: true,
      filter: {
        variants: false,
        archived: false,
      },
    });

    if (selection) {
      console.log("Selected products:", selection);
    }
  };

  return (
    <Button onClick={openResourcePicker}>Select Products</Button>
  );
}

Toast Notifications

import { useAppBridge } from "@shopify/app-bridge-react";
import { Toast } from "@shopify/app-bridge/actions";

function SaveButton() {
  const app = useAppBridge();

  const handleSave = async () => {
    try {
      await saveData();
      const toast = Toast.create(app, {
        message: "Settings saved successfully",
        duration: 3000,
      });
      toast.dispatch(Toast.Action.SHOW);
    } catch (error) {
      const toast = Toast.create(app, {
        message: "Error saving settings",
        duration: 5000,
        isError: true,
      });
      toast.dispatch(Toast.Action.SHOW);
    }
  };

  return <Button primary onClick={handleSave}>Save</Button>;
}

Polaris Design System

Common Patterns

import {
  Page,
  Layout,
  Card,
  FormLayout,
  TextField,
  Select,
  Button,
  Banner,
  Modal,
  ResourceList,
  ResourceItem,
  Avatar,
  TextStyle,
  Stack,
  Badge,
  Pagination,
} from "@shopify/polaris";

function SettingsPage() {
  const [formState, setFormState] = useState({
    apiKey: "",
    environment: "production",
  });
  const [loading, setLoading] = useState(false);
  const [showModal, setShowModal] = useState(false);

  return (
    <Page
      title="App Settings"
      primaryAction={{
        content: "Save",
        loading: loading,
        onAction: handleSave,
      }}
      secondaryActions={[
        { content: "Reset", onAction: handleReset },
      ]}
    >
      <Layout>
        <Layout.Section>
          <Banner
            title="Configuration required"
            status="warning"
            action={{ content: "Learn more", url: "/docs" }}
          >
            Please configure your API settings to enable all features.
          </Banner>
        </Layout.Section>

        <Layout.AnnotatedSection
          title="API Configuration"
          description="Configure your external API connection."
        >
          <Card>
            <Card.Section>
              <FormLayout>
                <TextField
                  label="API Key"
                  value={formState.apiKey}
                  onChange={(value) => setFormState({ ...formState, apiKey: value })}
                  type="password"
                  autoComplete="off"
                />
                <Select
                  label="Environment"
                  options={[
                    { label: "Production", value: "production" },
                    { label: "Sandbox", value: "sandbox" },
                  ]}
                  value={formState.environment}
                  onChange={(value) => setFormState({ ...formState, environment: value })}
                />
              </FormLayout>
            </Card.Section>
          </Card>
        </Layout.AnnotatedSection>

        <Layout.Section>
          <Card title="Connected Products">
            <ResourceList
              items={products}
              renderItem={(item) => (
                <ResourceItem
                  id={item.id}
                  media={<Avatar customer size="medium" source={item.image} />}
                  accessibilityLabel={`View details for ${item.title}`}
                >
                  <Stack>
                    <Stack.Item fill>
                      <TextStyle variation="strong">{item.title}</TextStyle>
                    </Stack.Item>
                    <Badge status={item.synced ? "success" : "warning"}>
                      {item.synced ? "Synced" : "Pending"}
                    </Badge>
                  </Stack>
                </ResourceItem>
              )}
            />
          </Card>
        </Layout.Section>
      </Layout>

      <Modal
        open={showModal}
        onClose={() => setShowModal(false)}
        title="Confirm action"
        primaryAction={{
          content: "Confirm",
          destructive: true,
          onAction: handleConfirm,
        }}
        secondaryActions={[
          { content: "Cancel", onAction: () => setShowModal(false) },
        ]}
      >
        <Modal.Section>
          Are you sure you want to proceed?
        </Modal.Section>
      </Modal>
    </Page>
  );
}

Testing

Unit Tests

// tests/webhooks.test.ts
import { describe, it, expect, vi } from "vitest";
import { action } from "../app/routes/webhooks";

describe("Webhook handlers", () => {
  it("handles product create webhook", async () => {
    const mockRequest = new Request("https://app.com/webhooks", {
      method: "POST",
      headers: {
        "X-Shopify-Topic": "products/create",
        "X-Shopify-Shop-Domain": "test-shop.myshopify.com",
        "X-Shopify-Hmac-Sha256": "valid-hmac",
      },
      body: JSON.stringify({
        id: 123456789,
        title: "Test Product",
        handle: "test-product",
      }),
    });

    const response = await action({ request: mockRequest, params: {}, context: {} });
    expect(response.status).toBe(200);
  });
});

Development

# Start development server with hot reload
npm run dev

# Generate GraphQL types
npm run shopify app generate types

# Test webhooks locally
npm run shopify app webhook trigger --topic PRODUCTS_CREATE

# Deploy to Shopify
npm run deploy

Related References

  • Storefront API - For customer-facing features
  • Checkout Customization - For checkout extensions
  • Liquid Templating - For theme app extensions

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk CRITICAL
  • Gen Agent Trust Hub16d

    This skill has been flagged as critical due to the presence of malicious URLs and files identified by automated scanners. A documentation link provided in the metadata is blacklisted, and a third-party script referenced in the performance optimization guide is associated with botnet activity. Additionally, the main skill file was flagged by file reputation scanners as potentially malicious.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    5/6 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at efebc44. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 5 months ago
Other metadata
metadata
{
  "author": "https://github.com/Jeffallan",
  "version": "1.1.0",
  "domain": "platform",
  "triggers": "Shopify, Liquid, Storefront API, Shopify Plus, Hydrogen, Shopify app, checkout extensions, Shopify Functions, App Bridge, theme development, e-commerce, Polaris",
  "role": "expert",
  "scope": "implementation",
  "output-format": "code",
  "related-skills": "react-expert, graphql-architect, api-designer"
}
  • shopify
  • liquid
  • storefront-api
  • theme-development
  • app-development
  • hydrogen
  • checkout-extensions
  • graphql
  • e-commerce
  • shopify-cli

README badge

README badge for jeffallan/claude-skills/shopify-expert

Builds and debugs Shopify themes with Liquid, develops custom Shopify apps with OAuth and webhooks, and implements Storefront API integrations for headless storefronts. Covers theme development with shopify-cli, app scaffolding, checkout UI extensions, and GraphQL queries against the Storefront and Admin APIs.

Generated from the current SKILL.md.

Does this skill cover both theme development and app development?
Yes. The skill handles Liquid theme templating, Storefront API integrations for headless storefronts, and custom Shopify app development including OAuth, webhooks, and checkout extensions.
What version of the Storefront API does this target?
The skill uses Storefront API 2024-10 or newer and requires Liquid 2.0 syntax for themes.
Can this skill help with checkout customization?
Yes. It covers checkout UI extensions, Shopify Functions, and custom checkout solutions, including sandbox testing and deployment workflows.
Does this skill support Hydrogen and headless storefronts?
Yes. The skill includes Storefront API GraphQL patterns, Hydrogen 2024 integration, and guidance for building custom React storefronts with proper image optimization and performance tuning.
What are the main constraints I should know about?
Must use Shopify CLI workflows, run `shopify theme check` before deployment, avoid hardcoded credentials and deprecated REST endpoints, and respect Storefront API rate limits of 2000 points per second.

Generated from the current SKILL.md. These answers refresh after source changes.