Agent Skills by ALSEL
Anthropic Claudeソフトウェア開発⭐ リポ 0品質スコア 50/100

shopify-apps

Remix/React Routerアプリ、App Bridgeを使った埋め込みアプリ、Webhookの処理、GraphQL Admin API、Polarisコンポーネント、課金機能、アプリ拡張など、Shopifyアプリ開発に必要な高度なパターンを網羅したスキルです。

description の原文を見る

Expert patterns for Shopify app development including Remix/React Router apps, embedded apps with App Bridge, webhook handling, GraphQL Admin API, Polaris components, billing, and app extensions.

SKILL.md 本文

Shopify Apps

Shopify アプリ開発の専門的なパターン。Remix/React Router アプリ、App Bridge を使用した埋め込みアプリ、webhook 処理、GraphQL Admin API、Polaris コンポーネント、課金、およびアプリ拡張機能が含まれます。

パターン

React Router アプリのセットアップ

React Router を使用したモダン Shopify アプリテンプレート

使用時期: 新しい Shopify アプリを開始する場合

テンプレート

# Shopify CLI で新しいアプリを作成
npm init @shopify/app@latest my-shopify-app

# プロジェクト構造
# my-shopify-app/
# ├── app/
# │   ├── routes/
# │   │   ├── app._index.tsx        # メインアプリページ
# │   │   ├── app.tsx               # プロバイダー付きアプリレイアウト
# │   │   ├── auth.$.tsx            # 認証コールバック
# │   │   └── webhooks.tsx          # Webhook ハンドラー
# │   ├── shopify.server.ts         # サーバー設定
# │   └── root.tsx                  # ルートレイアウト
# ├── extensions/                   # アプリ拡張機能
# ├── shopify.app.toml              # アプリ設定
# └── package.json
# shopify.app.toml
name = "my-shopify-app"
client_id = "your-client-id"
application_url = "https://your-app.example.com"

[access_scopes]
scopes = "read_products,write_products,read_orders"

[webhooks]
api_version = "2024-10"

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

[auth]
redirect_urls = ["https://your-app.example.com/auth/callback"]
// app/shopify.server.ts
import "@shopify/shopify-app-remix/adapters/node";
import {
  LATEST_API_VERSION,
  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!,
  scopes: process.env.SCOPES?.split(","),
  appUrl: process.env.SHOPIFY_APP_URL!,
  authPathPrefix: "/auth",
  sessionStorage: new PrismaSessionStorage(prisma),
  distribution: AppDistribution.AppStore,
  future: {
    unstable_newEmbeddedAuthStrategy: true,
  },
  ...(process.env.SHOP_CUSTOM_DOMAIN
    ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
    : {}),
});

export default shopify;
export const apiVersion = LATEST_API_VERSION;
export const authenticate = shopify.authenticate;
export const sessionStorage = shopify.sessionStorage;

注記

  • React Router は推奨テンプレートとして Remix に代わりました (2024 年末)
  • unstable_newEmbeddedAuthStrategy は新しいアプリではデフォルトで有効
  • Webhook は shopify.app.toml で設定され、コードではありません
  • 設定変更を適用するには「shopify app deploy」を実行してください

App Bridge を使用した埋め込みアプリ

Shopify Admin に埋め込まれた状態でアプリをレンダリング

使用時期: 埋め込み型管理アプリを構築する場合

テンプレート

// app/routes/app.tsx - プロバイダー付きアプリレイアウト
import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
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 async function loader({ request }: LoaderFunctionArgs) {
  await authenticate.admin(request);
  return json({ apiKey: process.env.SHOPIFY_API_KEY! });
}

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

  return (
    <AppProvider isEmbeddedApp apiKey={apiKey}>
      <ui-nav-menu>
        <Link to="/app" rel="home">ホーム</Link>
        <Link to="/app/products">商品</Link>
        <Link to="/app/settings">設定</Link>
      </ui-nav-menu>
      <Outlet />
    </AppProvider>
  );
}

export function ErrorBoundary() {
  const error = useRouteError();
  return (
    <AppProvider isEmbeddedApp>
      <Page>
        <Card>
          <Text as="p" variant="bodyMd">
            何か問題が発生しました。もう一度試してください。
          </Text>
        </Card>
      </Page>
    </AppProvider>
  );
}
// app/routes/app._index.tsx - メインアプリページ
import {
  Page,
  Layout,
  Card,
  Text,
  BlockStack,
  Button,
} from "@shopify/polaris";
import { TitleBar } from "@shopify/app-bridge-react";

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

  // GraphQL クエリ
  const response = await admin.graphql(`
    query {
      shop {
        name
        email
      }
    }
  `);

  const { data } = await response.json();
  return json({ shop: data.shop });
}

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

  return (
    <Page>
      <TitleBar title="My Shopify App" />
      <Layout>
        <Layout.Section>
          <Card>
            <BlockStack gap="200">
              <Text as="h2" variant="headingMd">
                {shop.name} へようこそ!
              </Text>
              <Text as="p" variant="bodyMd">
                アプリがこのストアに接続されました。
              </Text>
              <Button variant="primary">
                開始する
              </Button>
            </BlockStack>
          </Card>
        </Layout.Section>
      </Layout>
    </Page>
  );
}

注記

  • App Bridge は Built for Shopify に必須です (2025 年 7 月)
  • Polaris コンポーネントは Shopify Admin デザインと一致します
  • TitleBar とナビゲーションは App Bridge から提供されます
  • リクエストは常に authenticate.admin() で認証してください

Webhook 処理

HMAC 検証を使用したセキュアな webhook 処理

使用時期: Shopify webhook を受信する場合

テンプレート

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

export const action = async ({ request }: ActionFunctionArgs) => {
  // Webhook を認証 (HMAC 署名を検証)
  const { topic, shop, payload, admin } = await authenticate.webhook(request);

  console.log(`${shop} から ${topic} webhook を受信しました`);

  // トピックに基づいて処理
  switch (topic) {
    case "ORDERS_CREATE":
      // 非同期処理のためキューに登録
      await queueOrderProcessing(payload);
      break;

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

    case "APP_UNINSTALLED":
      // ストアデータをクリーンアップ
      await db.session.deleteMany({ where: { shop } });
      await db.shopData.delete({ where: { shop } });
      break;

    case "CUSTOMERS_DATA_REQUEST":
    case "CUSTOMERS_REDACT":
    case "SHOP_REDACT":
      // GDPR webhook - 必須
      await handleGDPRWebhook(topic, payload);
      break;

    default:
      console.log(`未処理の webhook トピック: ${topic}`);
  }

  // 重要: すぐに 200 を返す
  // Shopify は 5 秒以内のレスポンスを期待します
  return new Response(null, { status: 200 });
};

// レスポンス後に非同期で処理
async function queueOrderProcessing(payload: any) {
  // ジョブキュー (BullMQ など) を使用
  await jobQueue.add("process-order", {
    orderId: payload.id,
    orderData: payload,
  });
}

async function handleProductUpdate(shop: string, payload: any) {
  // 高速な同期操作のみ
  await db.product.upsert({
    where: { shopifyId: payload.id },
    update: {
      title: payload.title,
      updatedAt: new Date(),
    },
    create: {
      shopifyId: payload.id,
      shop,
      title: payload.title,
    },
  });
}

async function handleGDPRWebhook(topic: string, payload: any) {
  // GDPR コンプライアンス - 全アプリで必須
  switch (topic) {
    case "CUSTOMERS_DATA_REQUEST":
      // 30 日以内に顧客データを返す
      break;
    case "CUSTOMERS_REDACT":
      // 顧客データを削除
      break;
    case "SHOP_REDACT":
      // 全ストアデータを削除 (アンインストール後 48 時間)
      break;
  }
}

注記

  • 5 秒以内にレスポンスするか、webhook は失敗します
  • 重い処理にはジョブキューを使用してください
  • GDPR webhook は App Store で必須です
  • HMAC 検証は authenticate.webhook() で処理されます

GraphQL Admin API

GraphQL を使用してストアデータをクエリして変更

使用時期: Shopify Admin API と相互作用する場合

テンプレート

// 認証済み admin クライアントで GraphQL クエリを実行
export async function loader({ request }: LoaderFunctionArgs) {
  const { admin } = await authenticate.admin(request);

  // ページネーション付きで商品をクエリ
  const response = await admin.graphql(`
    query GetProducts($first: Int!, $after: String) {
      products(first: $first, after: $after) {
        edges {
          node {
            id
            title
            status
            totalInventory
            priceRangeV2 {
              minVariantPrice {
                amount
                currencyCode
              }
            }
            images(first: 1) {
              edges {
                node {
                  url
                  altText
                }
              }
            }
          }
          cursor
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  `, {
    variables: {
      first: 10,
      after: null,
    },
  });

  const { data } = await response.json();
  return json({ products: data.products });
}

// ミューテーション
export async function action({ request }: ActionFunctionArgs) {
  const { admin } = await authenticate.admin(request);
  const formData = await request.formData();
  const productId = formData.get("productId");
  const newTitle = formData.get("title");

  const response = await admin.graphql(`
    mutation UpdateProduct($input: ProductInput!) {
      productUpdate(input: $input) {
        product {
          id
          title
        }
        userErrors {
          field
          message
        }
      }
    }
  `, {
    variables: {
      input: {
        id: productId,
        title: newTitle,
      },
    },
  });

  const { data } = await response.json();

  if (data.productUpdate.userErrors.length > 0) {
    return json({
      errors: data.productUpdate.userErrors,
    }, { status: 400 });
  }

  return json({ product: data.productUpdate.product });
}

// 大規模データセット向けバルク操作
async function bulkUpdateProducts(admin: AdminApiContext) {
  // バルク操作を作成
  const response = await admin.graphql(`
    mutation {
      bulkOperationRunMutation(
        mutation: "mutation call($input: ProductInput!) {
          productUpdate(input: $input) { product { id } }
        }",
        stagedUploadPath: "path-to-staged-upload"
      ) {
        bulkOperation {
          id
          status
        }
        userErrors {
          message
        }
      }
    }
  `);

  // 完了をポーリングするか webhook を使用
  // BULK_OPERATIONS_FINISH webhook
}

注記

  • GraphQL は新しいパブリックアプリに必須です (2025 年 4 月)
  • レート制限: 60 秒当たり 1000 ポイント
  • 250 項目以上にはバルク操作を使用してください
  • App Bridge から直接 API アクセスが利用可能です

課金 API 統合

アプリのサブスクリプション課金を実装

使用時期: Shopify アプリを収益化する場合

テンプレート

// app/routes/app.billing.tsx
import { json, redirect } from "@remix-run/node";
import { Page, Card, Button, BlockStack, Text } from "@shopify/polaris";
import { authenticate } from "../shopify.server";

const PLANS = {
  basic: {
    name: "Basic",
    amount: 9.99,
    currencyCode: "USD",
    interval: "EVERY_30_DAYS",
  },
  pro: {
    name: "Pro",
    amount: 29.99,
    currencyCode: "USD",
    interval: "EVERY_30_DAYS",
  },
};

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

  // 現在のサブスクリプションをチェック
  const response = await admin.graphql(`
    query {
      currentAppInstallation {
        activeSubscriptions {
          id
          name
          status
          lineItems {
            plan {
              pricingDetails {
                ... on AppRecurringPricing {
                  price {
                    amount
                    currencyCode
                  }
                  interval
                }
              }
            }
          }
        }
      }
    }
  `);

  const { data } = await response.json();
  return json({
    subscription: data.currentAppInstallation.activeSubscriptions[0],
  });
}

export async function action({ request }: ActionFunctionArgs) {
  const { admin, session } = await authenticate.admin(request);
  const formData = await request.formData();
  const planKey = formData.get("plan") as keyof typeof PLANS;
  const plan = PLANS[planKey];

  // サブスクリプション料金を作成
  const response = await admin.graphql(`
    mutation CreateSubscription($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!, $returnUrl: URL!, $test: Boolean) {
      appSubscriptionCreate(
        name: $name
        lineItems: $lineItems
        returnUrl: $returnUrl
        test: $test
      ) {
        appSubscription {
          id
          status
        }
        confirmationUrl
        userErrors {
          field
          message
        }
      }
    }
  `, {
    variables: {
      name: plan.name,
      lineItems: [
        {
          plan: {
            appRecurringPricingDetails: {
              price: {
                amount: plan.amount,
                currencyCode: plan.currencyCode,
              },
              interval: plan.interval,
            },
          },
        },
      ],
      returnUrl: `https://${session.shop}/admin/apps/${process.env.SHOPIFY_API_KEY}`,
      test: process.env.NODE_ENV !== "production",
    },
  });

  const { data } = await response.json();

  if (data.appSubscriptionCreate.userErrors.length > 0) {
    return json({
      errors: data.appSubscriptionCreate.userErrors,
    }, { status: 400 });
  }

  // 料金承認のため加盟店をリダイレクト
  return redirect(data.appSubscriptionCreate.confirmationUrl);
}

export default function Billing() {
  const { subscription } = useLoaderData<typeof loader>();
  const submit = useSubmit();

  return (
    <Page title="課金">
      <Card>
        {subscription ? (
          <BlockStack gap="200">
            <Text as="p" variant="bodyMd">
              現在のプラン: {subscription.name}
            </Text>
            <Text as="p" variant="bodyMd">
              ステータス: {subscription.status}
            </Text>
          </BlockStack>
        ) : (
          <BlockStack gap="400">
            <Text as="h2" variant="headingMd">
              プランを選択
            </Text>
            <Button onClick={() => submit({ plan: "basic" }, { method: "post" })}>
              Basic - $9.99/月
            </Button>
            <Button onClick={() => submit({ plan: "pro" }, { method: "post" })}>
              Pro - $29.99/月
            </Button>
          </BlockStack>
        )}
      </Card>
    </Page>
  );
}

注記

  • 開発ストアでは test: true を使用してください
  • 加盟店がサブスクリプションを承認する必要があります
  • アプリあたり 1 つの定期料金 + 1 つの従量課金が最大です
  • 定期課金は 30 日間の課金周期です

アプリ拡張機能開発

Shopify チェックアウト、管理画面、またはストアフロントを拡張

使用時期: アプリ拡張機能を構築する場合

テンプレート

# shopify.extension.toml (extensions/my-extension/ 内)
api_version = "2024-10"

[[extensions]]
type = "ui_extension"
name = "Product Customizer"
handle = "product-customizer"

[[extensions.targeting]]
target = "admin.product-details.block.render"
module = "./src/AdminBlock.tsx"

[extensions.capabilities]
api_access = true

[extensions.settings]
[[extensions.settings.fields]]
key = "show_preview"
type = "boolean"
name = "プレビューを表示"
// extensions/my-extension/src/AdminBlock.tsx
import {
  reactExtension,
  useApi,
  useSettings,
  BlockStack,
  Text,
  Button,
  InlineStack,
} from "@shopify/ui-extensions-react/admin";

export default reactExtension(
  "admin.product-details.block.render",
  () => <ProductCustomizer />
);

function ProductCustomizer() {
  const { data, extension } = useApi<"admin.product-details.block.render">();
  const settings = useSettings();

  const productId = data?.selected?.[0]?.id;

  const handleCustomize = async () => {
    // 拡張機能からの API 呼び出し
    const result = await fetch("/api/customize", {
      method: "POST",
      body: JSON.stringify({ productId }),
    });
  };

  return (
    <BlockStack gap="base">
      <Text fontWeight="bold">Product Customizer</Text>
      <Text>
        商品をカスタマイズ: {productId}
      </Text>
      {settings.show_preview && (
        <Text size="small">プレビュー有効</Text>
      )}
      <InlineStack gap="base">
        <Button onPress={handleCustomize}>
          カスタマイズを適用
        </Button>
      </InlineStack>
    </BlockStack>
  );
}

// チェックアウト UI 拡張機能
// [[extensions.targeting]]
// target = "purchase.checkout.block.render"

// extensions/checkout-ext/src/Checkout.tsx
import {
  reactExtension,
  Banner,
  useCartLines,
  useTotalAmount,
} from "@shopify/ui-extensions-react/checkout";

export default reactExtension(
  "purchase.checkout.block.render",
  () => <CheckoutBanner />
);

function CheckoutBanner() {
  const cartLines = useCartLines();
  const total = useTotalAmount();

  if (total.amount > 100) {
    return (
      <Banner status="success">
        送料無料の対象です!
      </Banner>
    );
  }

  return null;
}

注記

  • 拡張機能はサンドボックス化された iframe で実行されます
  • React の場合は @shopify/ui-extensions-react を使用してください
  • フルアプリと比較して API が限定的です
  • 「shopify app deploy」でデプロイしてください

危険な落とし穴

Webhook は 5 秒以内にレスポンスする必要があります

重大度: 高

状況: Shopify から webhook を受信する

症状:

  • Webhook 配信がエラーとしてマークされる
  • Shopify ログに「アプリがタイムアウト内に応答しなかった」と表示される
  • 注文/商品の更新がない
  • Webhook が繰り返しリトライされてからキャンセルされる

原因: Shopify は 5 秒以内に 2xx レスポンスを期待します。アプリが webhook データを処理した後にレスポンスを返すと、タイムアウトします。

Shopify は失敗した webhook を 48 時間にわたり最大 19 回リトライします。 継続的に失敗すると、webhook は完全にキャンセルされることがあります。

重い処理 (API 呼び出し、データベース操作) はレスポンス送信後に 実行する必要があります。

推奨される修正:

すぐにレスポンスして非同期で処理

// app/routes/webhooks.tsx
export const action = async ({ request }: ActionFunctionArgs) => {
  const { topic, shop, payload } = await authenticate.webhook(request);

  // 非同期処理のためキューに登録
  await jobQueue.add("process-webhook", {
    topic,
    shop,
    payload,
  });

  // 重要: すぐに 200 を返す
  return new Response(null, { status: 200 });
};

// ワーカープロセスが実際の処理を実行
// workers/webhook-processor.ts
import { Worker } from "bullmq";

const worker = new Worker("process-webhook", async (job) => {
  const { topic, shop, payload } = job.data;

  switch (topic) {
    case "ORDERS_CREATE":
      await processOrder(shop, payload);
      break;
    // ... その他のハンドラー
  }
});

シンプルな操作は高速に

// シンプルなデータベース更新は高速であれば OK
export const action = async ({ request }: ActionFunctionArgs) => {
  const { topic, payload } = await authenticate.webhook(request);

  // 高速なデータベース更新 (< 1 秒)
  await db.product.update({
    where: { shopifyId: payload.id },
    data: { title: payload.title },
  });

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

Webhook パフォーマンスを監視

// レスポンス時間をログ
const start = Date.now();

await handleWebhook(payload);

const duration = Date.now() - start;
console.log(`Webhook は ${duration}ms で処理されました`);

// タイムアウト時間に近づく場合はアラート
if (duration > 3000) {
  console.warn("Webhook 処理に時間がかかっています!");
}

API レート制限が 429 エラーを引き起こす

重大度: 高

状況: Shopify への API 呼び出しを実行する

症状:

  • HTTP 429 Too Many Requests エラー
  • 「Throttled」レスポンス
  • アプリが応答しなくなる
  • 操作がサイレント失敗または部分的に失敗

原因: Shopify は厳しいレート制限を適用します:

  • REST: ストアあたり 2 リクエスト/秒
  • GraphQL: 60 秒当たり 1000 ポイント

制限を超えると、すぐに 429 エラーが発生します。 継続的な違反は一時的なバンになる可能性があります。

バルク操作も制限にカウントされます。

推奨される修正:

レート制限ヘッダーをチェック

// REST API
// X-Shopify-Shop-Api-Call-Limit: 39/40

// GraphQL - レスポンス拡張機能をチェック
const response = await admin.graphql(`...`);
const { data, extensions } = await response.json();

const cost = extensions?.cost;
// {
//   "requestedQueryCost": 42,
//   "actualQueryCost": 42,
//   "throttleStatus": {
//     "maximumAvailable": 1000,
//     "currentlyAvailable": 958,
//     "restoreRate": 50
//   }
// }

指数バックオフでリトライを実装

async function shopifyRequest(
  fn: () => Promise<Response>,
  maxRetries = 3
): Promise<Response> {
  let lastError: Error;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fn();

      if (response.status === 429) {
        // リトライ後ヘッダーまたはデフォルトを取得
        const retryAfter = parseInt(
          response.headers.get("Retry-After") || "2"
        );
        await sleep(retryAfter * 1000 * Math.pow(2, attempt));
        continue;
      }

      return response;
    } catch (error) {
      lastError = error as Error;
    }
  }

  throw lastError!;
}

大規模データセットにはバルク操作を使用

// 1000 個の個別呼び出しの代わりにバルク操作を使用
const response = await admin.graphql(`
  mutation {
    bulkOperationRunMutation(
      mutation: "mutation($input: ProductInput!) {
        productUpdate(input: $input) { product { id } }
      }",
      stagedUploadPath: "..."
    ) {
      bulkOperation { id status }
      userErrors { message }
    }
  }
`);

リクエストをキューに登録

import { RateLimiter } from "limiter";

// REST あたり 2 リクエスト/秒
const limiter = new RateLimiter({
  tokensPerInterval: 2,
  interval: "second",
});

async function rateLimitedRequest(fn: () => Promise<any>) {
  await limiter.removeTokens(1);
  return fn();
}

保護されたカスタマーデータには特別な権限が必要

重大度: 高

状況: Webhook または API でカスタマー PII にアクセスする

症状:

  • 注文/顧客向け webhook 配信が失敗する
  • カスタマーデータフィールドが null または空
  • 開発環境では動作するが、本番環境では失敗する
  • 「保護されたカスタマーデータアクセス」エラー

原因: 2024 年 4 月以降、保護されたカスタマーデータ (PII) へのアクセスには Shopify からの明示的な承認が必要です。これは OAuth スコープとは別です。

保護されたデータには以下が含まれます:

  • カスタマー名、メール、住所
  • 注文カスタマー情報
  • サブスクリプションカスタマー詳細

read_orders スコープがあっても、保護されたデータアクセス権限が ない限り、webhook からカスタマーデータを受け取りません。

推奨される修正:

保護されたカスタマーデータアクセスを要求

  1. パートナーダッシュボード > アプリ > API アクセスにアクセス
  2. 「保護されたカスタマーデータアクセス」の下
  3. 必要なデータタイプのアクセスを要求
  4. ユースケースを正当化
  5. Shopify の承認を待つ (数日かかる場合があります)

データアクセスレベルをチェック

// アプリのデータアクセスをクエリ
const response = await admin.graphql(`
  query {
    currentAppInstallation {
      accessScopes {
        handle
      }
    }
  }
`);

欠落したデータを適切に処理

// Webhook ペイロードにはフィールドが含まれていない可能性があります
async function processOrder(payload: any) {
  const customerEmail = payload.customer?.email;

  if (!customerEmail) {
    // カスタマーデータが利用できません
    // 保護されたアクセス権限なし、またはデータが削除されています
    console.log("カスタマーデータは利用できません");
    return;
  }

  await sendOrderConfirmation(customerEmail);
}

直接アクセスには Customer Account API を使用

// ログイン済みのカスタマーの場合、Admin API とは異なる
// Customer Account API 経由でデータにアクセス可能

重複した Webhook 定義が競合を引き起こす

重大度: 中

状況: TOML とコード両方で webhook を設定する

症状:

  • 重複した webhook 配信
  • 一部の webhook が 2 回ファイアする
  • Webhook サブスクリプション登録失敗
  • 予期しない webhook 動作

原因: Shopify アプリは 2 つの場所で webhook を定義できます:

  1. shopify.app.toml (宣言的、推奨)
  2. afterAuth フック内のコード (命令型、レガシー)

同じ webhook を両方の場所で定義すると以下が発生します:

  • 重複したサブスクリプション
  • 登録時のレースコンディション
  • アプリ更新時の競合

推奨される修正:

TOML のみを使用 (推奨)

# shopify.app.toml
[webhooks]
api_version = "2024-10"

[webhooks.subscriptions]
topics = [
  "orders/create",
  "orders/updated",
  "products/create",
  "products/update",
  "app/uninstalled"
]
uri = "/webhooks"

コードベースの登録を削除

// TOML を使用している場合はこのようにしないでください
const shopify = shopifyApp({
  // ...
  hooks: {
    afterAuth: async ({ session }) => {
      // ここから webhook 登録を削除
      // TOML に処理させてください
    },
  },
});

TOML 変更を適用するにはデプロイ

# デプロイ時に webhook が登録されます
shopify app deploy

現在のサブスクリプションをチェック

const response = await admin.graphql(`
  query {
    webhookSubscriptions(first: 50) {
      edges {
        node {
          id
          topic
          endpoint {
            ... on WebhookHttpEndpoint {
              callbackUrl
            }
          }
        }
      }
    }
  }
`);

Webhook URL の末尾スラッシュが 404 を引き起こす

重大度: 中

状況: Webhook エンドポイントをセットアップする

症状:

  • Webhook が 404 Not Found を返す
  • Webhook 配信がすぐに失敗
  • ローカル開発では動作するが本番環境では失敗
  • ログに /webhooks/ への要求が表示される (not /webhooks)

原因: Shopify は自動的に webhook URL に末尾スラッシュを追加します。 サーバーが /webhooks と /webhooks/ の両方を処理しない場合、 webhook は 404 になります。

末尾スラッシュについて厳密なフレームワークで一般的です。

推奨される修正:

両方の URL 形式を処理

// Remix/React Router - デフォルトで両方動作
// app/routes/webhooks.tsx は /webhooks を処理

// Express - ミドルウェアを追加
app.use((req, res, next) => {
  if (req.path.endsWith('/') && req.path.length > 1) {
    const query = req.url.slice(req.path.length);
    const safePath = req.path.slice(0, -1);
    res.redirect(301, safePath + query);
  }
  next();
});

Web サーバーを設定

# Nginx - 末尾スラッシュを削除
location ~ ^(.+)/$ {
  return 301 $1;
}

# またはハンドラーに書き込み
location /webhooks {
  try_files $uri $uri/ @webhooks;
}
location @webhooks {
  proxy_pass http://app:3000/webhooks;
}

両方の形式でテスト

# スラッシュなしでテスト
curl -X POST https://your-app.com/webhooks

# スラッシュ付きでテスト
curl -X POST https://your-app.com/webhooks/

REST API から GraphQL への必須マイグレーション (2025 年 4 月)

重大度: 高

状況: 新しいパブリックアプリを構築するか既存アプリを保守する

症状:

  • REST API 使用により App Store 申請が却下されます
  • コンソールに廃止予定の警告が表示されます
  • 一部の REST エンドポイントが動作しなくなります
  • GraphQL でのみ新機能が利用できます

原因: 2024 年 10 月以降、REST Admin API はレガシーです。 2025 年 4 月から、新しいパブリックアプリは GraphQL を使用する必要があります。

REST エンドポイントは既存アプリで引き続き動作しますが、 新機能は GraphQL のみです。

Metafields、バルク操作、および多くの新機能は GraphQL を必要とします。

推奨される修正:

すべての新しいコードに GraphQL を使用

// REST (レガシー)
const response = await fetch(
  `https://${shop}/admin/api/2024-10/products.json`,
  {
    headers: { "X-Shopify-Access-Token": token },
  }
);

// GraphQL (推奨)
const response = await admin.graphql(`
  query {
    products(first: 10) {
      edges {
        node {
          id
          title
        }
      }
    }
  }
`);

既存の REST 呼び出しをマイグレーション

// REST: GET /products/{id}.json
// GraphQL equivalent:
const response = await admin.graphql(`
  query GetProduct($id: ID!) {
    product(id: $id) {
      id
      title
      status
      variants(first: 10) {
        edges {
          node {
            id
            price
            inventoryQuantity
          }
        }
      }
    }
  }
`, {
  variables: { id: `gid://shopify/Product/${productId}` },
});

Webhook にも GraphQL を使用

# shopify.app.toml
[webhooks]
api_version = "2024-10"  # 最新の GraphQL バージョンを使用

App Bridge は Built for Shopify に必須 (2025 年 7 月)

重大度: 高

状況: 埋め込み型 Shopify アプリを構築する

症状:

  • 「Built for Shopify」プログラムからアプリが却下されます
  • アプリが admin に正しく表示されません
  • ナビゲーションとクローム機能の問題
  • App Bridge バージョンについての警告

原因: 2025 年 7 月 1 日から、「Built for Shopify」ステータスを 求める全アプリは最新版の App Bridge と埋め込みを使用する 必要があります。

古い App Bridge バージョンまたは埋め込まれていないアプリは、 Built for Shopify の利点 (より良いプレイスメント、バッジ) を失います。

Shopify は App Bridge と Polaris をバージョンなしのスクリプトタグで 提供するようになり、自動的に更新されます。

推奨される修正:

スクリプトタグで最新の App Bridge を使用

<!-- 自動的に最新の状態に保たれます -->
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>

React で AppProvider を使用

// app/routes/app.tsx
import { AppProvider } from "@shopify/shopify-app-remix/react";

export default function App() {
  return (
    <AppProvider isEmbeddedApp apiKey={apiKey}>
      <Outlet />
    </AppProvider>
  );
}

埋め込まれた認証戦略を有効化

// shopify.server.ts
const shopify = shopifyApp({
  // ...
  future: {
    unstable_newEmbeddedAuthStrategy: true,
  },
});

埋め込みステータスをチェック

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

function MyComponent() {
  const app = useAppBridge();
  const isEmbedded = app.hostOrigin !== window.location.origin;
}

GDPR Webhook がない場合 App Store 承認がブロック

重大度: 高

状況: App Store に申請する

症状:

  • アプリの申請が却下されます
  • 「GDPR webhook が実装されていない」エラー
  • コンプライアンス手動レビューが失敗
  • Data Request webhook が処理されません

原因: Shopify では全アプリが 3 つの GDPR webhook を処理することを 要求します:

  1. customers/data_request - カスタマーデータを提供
  2. customers/redact - カスタマーデータを削除
  3. shop/redact - 全ストアデータを削除

これらはアプリ作成時に自動的にサブスクライブされます。 データを保存しない場合でも、ハンドラーを実装する必要があります。

推奨される修正:

すべての GDPR ハンドラーを実装

// app/routes/webhooks.tsx
export const action = async ({ request }: ActionFunctionArgs) => {
  const { topic, payload, shop } = await authenticate.webhook(request);

  switch (topic) {
    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;
  }

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

async function handleDataRequest(shop: string, payload: any) {
  const customerId = payload.customer.id;

  // 30 日以内にカスタマーデータを返す
  // 通常は data_request.destination_url に送信
  const customerData = await db.customer.findUnique({
    where: { shopifyId: customerId, shop },
  });

  if (customerData) {
    // マーチャントに提供された URL またはメールに送信
    await sendDataToMerchant(payload.data_request, customerData);
  }
}

async function handleCustomerRedact(shop: string, payload: any) {
  const customerId = payload.customer.id;

  // カスタマーの個人データを削除
  await db.customer.deleteMany({
    where: { shopifyId: customerId, shop },
  });

  await db.order.updateMany({
    where: { customerId, shop },
    data: { customerEmail: null, customerName: null },
  });
}

async function handleShopRedact(shop: string, payload: any) {
  // ストアは 48+ 時間前にアンインストール
  // このストアの全データを削除
  await db.session.deleteMany({ where: { shop } });
  await db.customer.deleteMany({ where: { shop } });
  await db.order.deleteMany({ where: { shop } });
  await db.settings.deleteMany({ where: { shop } });
}

何も保存しない場合でも

// データがないが応答 200 する必要があります
case "CUSTOMERS_DATA_REQUEST":
case "CUSTOMERS_REDACT":
case "SHOP_REDACT":
  // データが保存されていませんが、承認する必要があります
  console.log(`GDPR ${topic} for ${shop} - データなし`);
  break;

検証チェック

ハードコードされた Shopify API シークレット

重大度: エラー

API シークレットをハードコードしてはいけません

メッセージ: ハードコードされた Shopify API シークレット。環境変数を使用してください。

ハードコードされた Shopify API キー

重大度: エラー

API キーは環境変数を使用する必要があります

メッセージ: ハードコードされた Shopify API キー。環境変数を使用してください。

HMAC 検証がない

重大度: エラー

Webhook エンドポイントは HMAC 署名を検証する必要があります

メッセージ: HMAC 検証なしの Webhook ハンドラー。authenticate.webhook() を使用してください。

同期 Webhook 処理

重大度: 警告

Webhook ハンドラーは迅速にレスポンスする必要があります

メッセージ: Webhook ハンドラーで複数の await 呼び出し。非同期処理を検討してください。

Webhook レスポンスがない

重大度: エラー

Webhook は 200 ステータスを返す必要があります

メッセージ: Webhook ハンドラーが適切なレスポンスを返していない可能性があります。

重複した Webhook 登録

重大度: 警告

Webhook は TOML でのみ定義する必要があります

メッセージ: コードベースの Webhook 登録。shopify.app.toml で Webhook を定義してください。

REST API 使用

重大度: 情報

REST API は廃止予定です。GraphQL を使用してください

メッセージ: REST API の使用が検出されました。GraphQL への移行を検討してください。

レート制限処理がない

重大度: 警告

API 呼び出しは 429 レスポンスを処理する必要があります

メッセージ: レート制限処理なしの API 呼び出し。リトライロジックを実装してください。

メモリ内セッションストレージ

重大度: 警告

メモリ内セッションはスケーリングしません

メッセージ: メモリ内セッションストレージ。PrismaSessionStorage などを使用してください。

セッション検証がない

重大度: エラー

ルートはセッションを検証する必要があります

メッセージ: 認証なしの Loader。authenticate.admin(request) を使用してください。

コラボレーション

委譲トリガー

  • ユーザーが支払い処理を必要とする -> stripe-integration (Shopify Payments または Stripe 統合)
  • ユーザーがカスタム認証を必要とする -> auth-specialist (Shopify OAuth を超える認証)
  • ユーザーがメール/SMS 通知を必要とする -> twilio-communications (Shopify 外顧客通知)
  • ユーザーが AI 機能を必要とする -> llm-architect (商品説明、チャットボット)
  • ユーザーがサーバーレスデプロイを必要とする -> aws-serverless (Lambda または Vercel デプロイ)

使用時期

  • ユーザーが次を提及または暗に示した場合: shopify app
  • ユーザーが次を提及または暗に示した場合: shopify
  • ユーザーが次を提及または暗に示した場合: embedded app
  • ユーザーが次を提及または暗に示した場合: polaris
  • ユーザーが次を提及または暗に示した場合: app bridge
  • ユーザーが次を提及または暗に示した場合: shopify webhook

制限事項

  • このスキルは、上記で説明されたスコープと明確に一致する場合にのみ使用してください。
  • 出力を環境固有の検証、テスト、または専門家レビューの代替として扱わないでください。
  • 必要な入力、権限、安全性の境界、または成功基準が不足している場合は、明確にしてください。

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
sickn33
リポジトリ
sickn33/antigravity-awesome-skills
ライセンス
MIT
最終更新
不明

Source: https://github.com/sickn33/antigravity-awesome-skills / ライセンス: MIT

関連スキル

汎用ソフトウェア開発⭐ リポ 39,967

doubt-driven-development

重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 1,175

apprun-skills

TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。

by yysun
OpenAIソフトウェア開発⭐ リポ 797

desloppify

コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。

by Git-on-my-level
汎用ソフトウェア開発⭐ リポ 39,967

debugging-and-error-recovery

テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

test-driven-development

テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

incremental-implementation

変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。

by addyosmani
本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: sickn33 · sickn33/antigravity-awesome-skills · ライセンス: MIT