mcpField
S
shopify-app-development

shopify-app-development

github.com/finsilabs開発6076 回インストール6 日前に更新
リポジトリ

shopify-app-development について

Shopify CLI 3.x の Remix テンプレートを使うアプリ開発者向けのスキルです。埋め込みアプリ、OAuth、Admin API、Polaris UI の構築を扱います。

主な機能

  1. 01Remix アプリのスキャフォールディング
  2. 02OAuth とセッション管理
  3. 03Admin API アクセス
  4. 04Polaris UI の構築ガイド
  5. 05Webhook とデプロイのガイド

活用シーン

  1. 01公開またはカスタム Shopify アプリの開始
  2. 02マーチャント向け埋め込みアプリの構築
  3. 03認証済み Admin API 操作の追加
  4. 04Express ベースの Shopify アプリの刷新

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill shopify-app-development

インストール後、Claude Code で/shopify-app-developmentを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

tile.json
evals/embedded-ui-with-polaris-and-app-bridge/
criteria.json
task.md
evals/gdpr-webhooks-and-hmac-validation/
criteria.json
task.md
evals/production-session-storage-and-api-versi/
criteria.json
task.md

詳細

Shopify アプリ開発

概要

Shopify CLI 3.x の Remix テンプレートを使用して Shopify アプリを構築します。このテンプレートは、OAuth トークンの交換、セッションストレージ、App Bridge の初期化を自動的に処理します。埋め込みアプリは Shopify Admin の iframe 内で動作し、Polaris を使用してネイティブアプリのような UI を実現します。現在の推奨方法では、従来の Express テンプレートではなく、Remix ベースの @shopify/shopify-app-remix パッケージを使用します。

このスキルを使用する場面

  • Admin の機能を拡張する公開 Shopify アプリやカスタム Shopify アプリを構築する場合
  • マーチャントが Shopify App Store からインストールする埋め込みアプリを作成する場合
  • 再インストール後もセッションを維持できる OAuth を初めて実装する場合
  • 認証済みマーチャントに代わって Admin API にアクセスする必要がある場合
  • Shopify の Polaris デザインシステムを使用して、マーチャント向けツールを構築する場合
  • 古い Express/koa ベースの Shopify アプリを、最新の Remix スタックに置き換える場合

基本的な手順

  1. Shopify CLI でアプリのひな形を作成する

    npm install -g @shopify/cli @shopify/theme
    shopify app init my-shopify-app
    # Choose: Remix template
    cd my-shopify-app
    shopify app dev
    

    OAuth、セッションストレージ(デフォルトでは SQLite)、App Bridge があらかじめ設定された Remix アプリのひな形が作成されます。開発コマンドは Cloudflare 経由でローカルサーバーをトンネルし、Partner 開発ストアにアプリをインストールします。

  2. OAuth フローとセッション処理を理解する

    ひな形では @shopify/shopify-app-remix を使用しており、OAuth の一連の処理を行います。app/shopify.server.ts では、次のように設定します。

    import "@shopify/shopify-app-remix/adapters/node";
    import {
      AppDistribution,
      DeliveryMethod,
      shopifyApp,
      LATEST_API_VERSION,
    } from "@shopify/shopify-app-remix/server";
    import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
    import { PrismaClient } from "@prisma/client";
    
    const prisma = new PrismaClient();
    
    const shopify = shopifyApp({
      apiKey: process.env.SHOPIFY_API_KEY,
      apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
      apiVersion: LATEST_API_VERSION,
      scopes: process.env.SCOPES?.split(","),
      appUrl: process.env.SHOPIFY_APP_URL || "",
      authPathPrefix: "/auth",
      sessionStorage: new PrismaSessionStorage(prisma),
      distribution: AppDistribution.AppStore,
      webhooks: {
        APP_UNINSTALLED: {
          deliveryMethod: DeliveryMethod.Http,
          callbackUrl: "/webhooks",
        },
      },
      hooks: {
        afterAuth: async ({ session }) => {
          shopify.registerWebhooks({ session });
        },
      },
    });
    
    export default shopify;
    export const authenticate = shopify.authenticate;
    
  3. ルートを保護し、Admin API を呼び出す

    Admin API へのアクセスが必要な loader や action では、authenticate.admin を呼び出します。

    // app/routes/app._index.tsx
    import { json } from "@remix-run/node";
    import { useLoaderData } from "@remix-run/react";
    import { authenticate } from "../shopify.server";
    
    export const loader = async ({ request }: LoaderFunctionArgs) => {
      const { admin, session } = await authenticate.admin(request);
    
      // GraphQL Admin API call
      const response = await admin.graphql(`
        query {
          shop {
            name
            email
            primaryDomain { url }
          }
        }
      `);
      const { data } = await response.json();
      return json({ shop: data.shop });
    };
    
    export default function Index() {
      const { shop } = useLoaderData<typeof loader>();
      return <Page title={`Hello, ${shop.name}`} />;
    }
    
  4. Polaris コンポーネントで UI を構築する

    // app/routes/app.products.tsx
    import {
      Page,
      Layout,
      Card,
      DataTable,
      Button,
      Banner,
    } from "@shopify/polaris";
    import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
    
    export default function ProductsPage() {
      const shopify = useAppBridge();
    
      const handleSave = async () => {
        // Use App Bridge Toast for notifications inside the iframe
        shopify.toast.show("Products updated successfully");
      };
    
      return (
        <Page>
          <TitleBar title="Products" primaryAction={{ content: "Save", onAction: handleSave }} />
          <Layout>
            <Layout.Section>
              <Card>
                <DataTable
                  columnContentTypes={["text", "numeric", "numeric"]}
                  headings={["Product", "Price", "Inventory"]}
                  rows={[["Widget A", "$19.99", 42]]}
                />
              </Card>
            </Layout.Section>
          </Layout>
        </Page>
      );
    }
    
  5. スコープを設定し、アプリの再インストールに対応する

    必要なスコープを shopify.app.toml で定義します。

    name = "my-shopify-app"
    client_id = "your_api_key"
    application_url = "https://your-app.fly.dev"
    embedded = true
    
    [access_scopes]
    scopes = "read_products,write_products,read_orders"
    
    [webhooks]
    api_version = "2025-01"
    
    [[webhooks.subscriptions]]
    topics = ["app/uninstalled"]
    uri = "/webhooks"
    

    アプリに個人データを保存していない場合でも、GDPR で必須の Webhook(customers/data_request、customers/redact、shop/redact)に対応します。Shopify では、これらのエンドポイントが必須です。

  6. 本番環境にデプロイする

    shopify app deploy
    # Deploys to Shopify (functions/extensions)
    # Deploy the Remix server separately (Fly.io, Railway, Render)
    fly launch
    fly deploy
    

例

Admin GraphQL API を使用したミューテーション
// Create a product via the Admin API inside a Remix action
export const action = async ({ request }: ActionFunctionArgs) => {
  const { admin } = await authenticate.admin(request);

  const response = await admin.graphql(
    `#graphql
    mutation CreateProduct($input: ProductInput!) {
      productCreate(input: $input) {
        product {
          id
          title
          handle
        }
        userErrors {
          field
          message
        }
      }
    }`,
    {
      variables: {
        input: {
          title: "New Product",
          vendor: "My Store",
          productType: "Widget",
          tags: ["new", "featured"],
        },
      },
    }
  );

  const { data } = await response.json();
  if (data.productCreate.userErrors.length > 0) {
    return json({ errors: data.productCreate.userErrors }, { status: 422 });
  }
  return json({ product: data.productCreate.product });
};
App Bridge のリソースピッカー(v4)
import { useAppBridge } from "@shopify/app-bridge-react";
import { useState } from "react";
import { Button } from "@shopify/polaris";

export default function ProductSelector() {
  const shopify = useAppBridge();
  const [selected, setSelected] = useState<string[]>([]);

  const handleSelectProducts = async () => {
    const selection = await shopify.resourcePicker({
      type: "product",
      multiple: true,
    });

    if (selection) {
      setSelected(selection.map((p) => p.id));
    }
  };

  return (
    <>
      <Button onClick={handleSelectProducts}>Select Products</Button>
      <p>Selected IDs: {selected.join(", ")}</p>
    </>
  );
}

ベストプラクティス

  • Remix CLI テンプレートを使用する — セッションストレージ、CSRF、OAuth トークンの更新、frame-ancestor CSP ヘッダーを自動的に処理します
  • セッションを永続データベースに保存する(本番環境では Prisma + PostgreSQL) — デフォルトの SQLite ストレージは、複数インスタンスでのデプロイには適していません
  • スコープを増やしすぎるとコンバージョンに影響する — 必要最小限のスコープのみをリクエストします。マーチャントにはインストール時にスコープ一覧が表示されます
  • ナビゲーションやモーダルには App Bridge を使用する — window.location による直接のナビゲーションは、埋め込み iframe のコンテキストを壊します
  • Webhook の HMAC 署名を検証する — 処理を行わない必須の GDPR Webhook でも検証します
  • 再インストールのフローをテストする — アプリをアンインストールして再インストールしたマーチャントには、古いセッションデータではなく、新しい OAuth トークンが付与される必要があります
  • 開発環境でのみ LATEST_API_VERSION を使用する — 破壊的変更を避けるため、本番環境では特定のバージョン(例:2025-01)に固定します
  • payment_required エラーに対応する — マーチャントがプランの上限を超えると、App Store のアプリで課金要件に関するエラーが発生することがあります

よくある問題

問題解決方法
「Refused to display in frame」CSP エラーRemix サーバーが Content-Security-Policy で frame-ancestors https://*.myshopify.com https://admin.shopify.com を返すことを確認してください
インストール後に OAuth リダイレクトがループするshopify.app.toml のアプリ URL が、サーバーにアクセスできる URL と一致していることを確認してください。一致しないとリダイレクトが無限に繰り返されます
後続のリクエストでセッションが見つからない永続的なセッションストレージ(Prisma/PostgreSQL)を使用してください。SQLite は Fly.io や Render のインスタンス間で機能しません
App Bridge の useAppBridge() が null を返す@shopify/shopify-app-remix/react の <AppProvider> で Remix アプリのルートをラップしてください
Webhook が登録されているのに発火しないafterAuth 中に登録された Webhook は、アプリの更新後に保持されない場合があります。確認用に別のルートで shopify.registerWebhooks を呼び出してください
Webhook エンドポイントで「Invalid HMAC」が表示されるJSON 解析ミドルウェアを適用する前に、生のボディを読み込むことを確認してください。Express/Remix のボディ解析の前に getRawBody を使用してください

関連スキル

  • @shopify-admin-api
  • @shopify-webhooks
  • @shopify-checkout-extensions
  • @shopify-storefront-api
  • @oauth-implementation

コメント