メインコンテンツにスキップ

Remix と Polaris を使用してカスタム Shopify アプリを構築する

Remix、Shopify CLI 3、Polaris デザイン システム、組み込み管理エクスペリエンスのための App Bridge を使用してカスタム Shopify アプリを構築するための完全な開発者ガイド。

E
ECOSIRE Research and Development Team
|2026年3月19日8 分で読める1.6k 語数|

Shopify のデフォルト アプリ フレームワークとしての Remix への移行は、プラットフォーム上で本番アプリを構築する方法の根本的な変化を示しています。 Remix ベースの Shopify アプリ テンプレートは、サーバー側のレンダリング、ストリーミング、ネストされたルーティング、およびネイティブの Shopify CLI 統合を単一の一貫したスタックで提供し、2023 年までアプリ開発の主流を占めていた古い Node/Express + React パターンを置き換えます。

このガイドでは、スキャフォールディング、認証、GraphQL データの取得、Polaris コンポーネントの統合、Webhook、請求、および保守可能でパフォーマンスの高い組み込みアプリを出荷するために Shopify 開発者が使用する経験豊富なパターンなど、完全な開発ライフサイクルを順を追って説明します。

重要なポイント

  • Shopify CLI 3.x は、OAuth、セッション ストレージ、App Bridge が事前構成された本番環境に対応した Remix アプリを生成します
  • authenticate.admin ヘルパーは OAuth フロー全体を処理します - OAuth を手動で実装しないでください
  • Remix のローダー/アクション パターンは Shopify の GraphQL リクエスト/ミューテーション サイクルに完全にマップされます
  • App Store の承認には Polaris コンポーネントが必要です - Polaris 標準を無視したカスタム UI は審査に合格しません
  • 信頼性を確保するために、Web フックは API 呼び出しだけでなくアプリ構成経由で登録する必要があります
  • Shopify Admin GraphQL API はページ分割されます - データローダーでは常にカーソルベースのページ分割を処理します
  • App Bridge は、クロムのない埋め込みエクスペリエンスを提供します。ナビゲーションに useAppBridge フックを使用する
  • サブスクリプション料金を請求するアプリには Billing API の統合が必要です

プロジェクトのセットアップと足場

Shopify CLI 3 から始めて、適切に構成された Remix アプリをスキャフォールディングします。

npm install -g @shopify/cli@latest
shopify app create node --template remix
cd your-app-name

生成された足場には以下が含まれます。

  • remix.config.js (Shopify 互換設定)
  • shopify.app.toml — アプリ構成ファイル (アプリ設定の .env を置き換えます)
  • app/shopify.server.ts — 集中認証および API クライアント構成
  • app/routes/app.tsx — ルート埋め込みアプリのレイアウト
  • prisma/schema.prisma — SQLite セッション ストレージ (運用環境では PostgreSQL に置き換えます)

shopify.app.toml 構造:

name = "your-app-name"
client_id = "your-api-key"
application_url = "https://your-app-url.com"
embedded = true

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

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

[webhooks]
api_version = "2025-01"

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

authenticate.admin による認証

shopify.server.ts ファイルは、すべての認証に関する問題を処理する authenticate オブジェクトをエクスポートします。 OAuth を手動で実装しないでください。authenticate.admin ヘルパーが以下を含むフロー全体を管理します。

  • OAuthコード交換
  • セッションの永続性
  • トークンのリフレッシュ
  • オンライン アクセス モードとオフライン アクセス モード
  • 販売者の同意収集

ルート ローダーでの認証の使用:

// app/routes/app.products.tsx
import { json } from "@remix-run/node";
import { authenticate } from "../shopify.server";

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

  const response = await admin.graphql(`
    #graphql
    query GetProducts($first: Int!) {
      products(first: $first) {
        nodes {
          id
          title
          status
          priceRangeV2 {
            minVariantPrice {
              amount
              currencyCode
            }
          }
          totalInventory
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  `, {
    variables: { first: 50 }
  });

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

オンライン セッションとオフライン セッション:

  • オフライン アクセス: ほとんどのアプリのデフォルト。セッションは販売者のログインとは関係なく持続します。バックグラウンド ジョブ、Webhook、スケジュールされたタスクに使用されます。
  • オンライン アクセス: ログインしている販売者ユーザーに関連付けられたセッション。特定のユーザーに代わって行動する必要がある場合に必要です (例: どのスタッフ メンバーがアクションを実行したかを追跡する)。

shopify.server.ts で構成します。

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, // Token Exchange (2024+)
  },
});

トークン交換 (2026 年に推奨)

Shopify の新しいトークン交換認証戦略により、埋め込みアプリの OAuth リダイレクトが排除されます。 Shopify 管理内のユーザーは、一度離れて戻る必要はありません。App Bridge からのセッション トークンは、サーバー側の API アクセス トークンと直接交換されます。これを unstable_newEmbeddedAuthStrategy: true で有効にします。


GraphQL データ取得パターン

Shopify の管理 API は GraphQL ファーストです。これらのパターンをマスターして、本番品質のデータを取得します。

カーソルベースのページネーション:

export async function getAllProducts(admin: AdminApiContext) {
  let hasNextPage = true;
  let cursor: string | null = null;
  const allProducts = [];

  while (hasNextPage) {
    const response = await admin.graphql(`
      #graphql
      query GetProducts($first: Int!, $after: String) {
        products(first: $first, after: $after) {
          nodes {
            id
            title
            handle
          }
          pageInfo {
            hasNextPage
            endCursor
          }
        }
      }
    `, {
      variables: { first: 250, after: cursor }
    });

    const data = await response.json();
    const { nodes, pageInfo } = data.data.products;

    allProducts.push(...nodes);
    hasNextPage = pageInfo.hasNextPage;
    cursor = pageInfo.endCursor;
  }

  return allProducts;
}

エラー処理を伴う突然変異:

export const action = async ({ request }: ActionFunctionArgs) => {
  const { admin } = await authenticate.admin(request);
  const formData = await request.formData();

  const title = formData.get("title") as string;
  const price = formData.get("price") as string;

  const response = await admin.graphql(`
    #graphql
    mutation CreateProduct($input: ProductInput!) {
      productCreate(input: $input) {
        product {
          id
          title
        }
        userErrors {
          field
          message
        }
      }
    }
  `, {
    variables: {
      input: {
        title,
        variants: [{ price }]
      }
    }
  });

  const data = await response.json();

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

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

大規模なデータセットの一括操作:

数千のレコードに対する操作の場合は、ページ分割されたクエリではなく、Bulk Operations API を使用します。一括クエリは非同期で実行され、JSONL ファイルを返します。

const bulkQuery = await admin.graphql(`
  mutation {
    bulkOperationRunQuery(
      query: """
        {
          products {
            edges {
              node {
                id
                title
                variants {
                  edges {
                    node {
                      id
                      price
                      inventoryQuantity
                    }
                  }
                }
              }
            }
          }
        }
      """
    ) {
      bulkOperation {
        id
        status
      }
      userErrors {
        field
        message
      }
    }
  }
`);

currentBulkOperation を status === "COMPLETED" までポーリングし、その後 url から JSONL をダウンロードして解析します。


Polaris デザイン システムの統合

Polaris は、Shopify の組み込みアプリ用の設計システムです。 App Store への提出には、Polaris の使用が必須です。Polaris 以外の UI はレビューが拒否されます。

Remix での Polaris のセットアップ:

// app/root.tsx
import { AppProvider } from "@shopify/polaris";
import "@shopify/polaris/build/esm/styles.css";
import translations from "@shopify/polaris/locales/en.json";

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

Shopify アプリの一般的な Polaris コンポーネント パターン:

// Product listing with DataTable
import {
  Page,
  Card,
  DataTable,
  Button,
  Badge,
  Filters,
  EmptyState
} from "@shopify/polaris";

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

  const rows = products.nodes.map(product => [
    product.title,
    <Badge tone={product.status === 'ACTIVE' ? 'success' : 'warning'}>
      {product.status}
    </Badge>,
    product.totalInventory,
    `${product.priceRangeV2.minVariantPrice.currencyCode} ${product.priceRangeV2.minVariantPrice.amount}`,
    <Button url={`/app/products/${product.id}`}>Edit</Button>
  ]);

  return (
    <Page
      title="Products"
      primaryAction={{ content: "Add product", url: "/app/products/new" }}
    >
      <Card>
        <DataTable
          columnContentTypes={['text', 'text', 'numeric', 'numeric', 'text']}
          headings={['Title', 'Status', 'Inventory', 'Price', 'Actions']}
          rows={rows}
        />
      </Card>
    </Page>
  );
}

Polaris および Remix によるフォーム処理:

import { Form, useNavigation, useActionData } from "@remix-run/react";
import { TextField, Select, FormLayout, Banner } from "@shopify/polaris";

export default function ProductForm() {
  const actionData = useActionData<typeof action>();
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <Form method="post">
      <FormLayout>
        {actionData?.errors && (
          <Banner tone="critical">
            {actionData.errors.map(e => <p key={e.field}>{e.message}</p>)}
          </Banner>
        )}
        <TextField
          label="Product title"
          name="title"
          autoComplete="off"
        />
        <TextField
          label="Price"
          name="price"
          type="number"
          prefix="$"
          autoComplete="off"
        />
        <Button submit loading={isSubmitting}>Save product</Button>
      </FormLayout>
    </Form>
  );
}

Webhook: 登録と処理

マーチャントストアのイベントに反応するアプリにとって、信頼性の高い Webhook 処理は非常に重要です。

shopify.app.toml による Webhook の登録:

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

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

Remix ルートでの Webhook の処理:

// app/routes/webhooks.tsx
import { authenticate } from "../shopify.server";
import db from "../db.server";

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

  switch (topic) {
    case "PRODUCTS_CREATE":
      await db.product.create({
        data: {
          shopifyId: payload.id.toString(),
          title: payload.title,
          shop: shop,
        }
      });
      break;

    case "APP_UNINSTALLED":
      if (session) {
        await db.session.deleteMany({ where: { shop } });
      }
      break;

    default:
      throw new Response("Unhandled webhook topic", { status: 404 });
  }

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

Webhook の信頼性に関するベスト プラクティス:

  1. 200 応答をすぐに返します。操作が遅い場合は、バックグラウンド キューを介して非同期に処理します。
  2. Webhook の X-Shopify-Webhook-Id ヘッダーを使用して冪等性を実装します。
  3. HMAC 署名を検証します (authenticate.webhook によって処理されます)
  4. 再試行ロジックの処理 — Shopify は失敗した Webhook を 48 時間にわたって最大 19 回再試行します
  5. デバッグのためにすべての Webhook ペイロードをログに記録します (ログの前に PII を削除します)。

Billing API: サブスクリプションと使用料金

アプリの料金請求には Shopify の Billing API を使用する必要があります。これをバイパスすると、App Store のポリシーに違反します。

アプリの 1 回限りの購入:

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

  const { hasActivePayment, appSubscription } = await billing.check({
    plans: ["Professional Plan"],
    isTest: process.env.NODE_ENV !== "production",
  });

  if (!hasActivePayment) {
    await billing.request({
      plan: "Professional Plan",
      isTest: process.env.NODE_ENV !== "production",
    });
  }

  return json({ session });
};

shopify.server.ts で料金プランを構成します:

const shopify = shopifyApp({
  // ...other config
  billing: {
    "Professional Plan": {
      amount: 29.99,
      currencyCode: "USD",
      interval: BillingInterval.Every30Days,
    },
    "Enterprise Plan": {
      amount: 99.99,
      currencyCode: "USD",
      interval: BillingInterval.Every30Days,
    }
  }
});

アプリブリッジと埋め込みナビゲーション

App Bridge は、Shopify 管理画面内で埋め込み iframe エクスペリエンスを管理する JavaScript ライブラリです。

主要な App Bridge パターン:

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

// Navigate to an external URL from embedded context
function ExternalLinkButton() {
  const app = useAppBridge();

  const handleClick = () => {
    const redirect = Redirect.create(app);
    redirect.dispatch(Redirect.Action.REMOTE, {
      url: "https://your-docs-site.com",
      newContext: true
    });
  };

  return <Button onClick={handleClick}>View documentation</Button>;
}

// Toast notifications
import { useToast } from "@shopify/app-bridge-react";

function SaveButton() {
  const { show } = useToast();
  const fetcher = useFetcher();

  useEffect(() => {
    if (fetcher.state === "idle" && fetcher.data?.success) {
      show("Settings saved");
    }
  }, [fetcher.state, fetcher.data]);

  return (
    <Button
      onClick={() => fetcher.submit(formData, { method: "post" })}
      loading={fetcher.state !== "idle"}
    >
      Save
    </Button>
  );
}

テストとローカル開発

# Start the app in development mode
shopify app dev

# The CLI handles:
# - ngrok tunnel creation for webhook delivery
# - App installation on your development store
# - Hot module replacement for Remix routes
# - Shopify Partner Dashboard app URL updates

GraphQL クエリのテスト:

コードにクエリを実装する前に、開発ストア管理者で Shopify GraphiQL アプリを使用してクエリをテストします。 your-store.myshopify.com/admin/apps/graphiql 経由でアクセスします。

単体テストのルート ローダー:

import { describe, it, expect, vi } from "vitest";
import { loader } from "~/routes/app.products";

vi.mock("~/shopify.server", () => ({
  authenticate: {
    admin: vi.fn().mockResolvedValue({
      admin: {
        graphql: vi.fn().mockResolvedValue({
          json: () => Promise.resolve({
            data: {
              products: {
                nodes: [{ id: "gid://shopify/Product/1", title: "Test Product" }],
                pageInfo: { hasNextPage: false, endCursor: null }
              }
            }
          })
        })
      },
      session: { shop: "test-shop.myshopify.com" }
    })
  }
}));

describe("Products loader", () => {
  it("returns product list", async () => {
    const request = new Request("https://test.com/app/products");
    const response = await loader({ request, params: {}, context: {} });
    const data = await response.json();

    expect(data.products.nodes).toHaveLength(1);
    expect(data.products.nodes[0].title).toBe("Test Product");
  });
});

よくある質問

新しい Shopify アプリには Remix テンプレートまたは Node/Express テンプレートを使用する必要がありますか?

すべての新しいアプリに Remix テンプレートを使用します。 Shopify は、アプリ開発ツールを Remix で標準化しています。Shopify CLI、ドキュメント、App Bridge React はすべて Remix 用に最適化されています。 Node/Express テンプレートは現在は積極的に開発されておらず、トークン交換などのいくつかの最新の認証機能がありません。既存の Node/Express アプリをすぐに移行する必要はありませんが、すべての新しいアプリは Remix から開始する必要があります。

Polaris の代わりに別のフロントエンド フレームワークを使用できますか?

アプリの公開部分 (独自ドメインの設定ページ、ランディング ページなど) には独自のデザイン システムを使用できますが、Shopify 管理内の埋め込みセクションでは Polaris を使用する必要があります。アプリレビューのガイドラインでは、埋め込み UI に Polaris を明示的に要求しています。カスタム コンポーネントを使用して Polaris を視覚的に複製しようとすると、通常、動作とアクセシビリティの不一致によりレビューが拒否されます。

アプリで Shopify API のレート制限を処理するにはどうすればよいですか?

Shopify の GraphQL Admin API は、「バケット」レート制限モデルを使用します。各ショップはアプリにバケットあたり 1,000 コスト ポイントを与え、これは 1 秒あたり 50 ポイントで回復されます。 GraphQL 応答の extensions.cost.throttleStatus フィールドを監視します。一括操作 (数千のレコードに影響する) の場合は、個別のレート制限がある一括操作 API を使用します。 429 応答を受信した場合は、ジッターを伴う指数バックオフを実装します。

本番環境の Shopify アプリにはどのデータベースを使用する必要がありますか?

本番環境ではデフォルトの SQLite を PostgreSQL に置き換えます。 Prisma セッション ストレージ アダプターは PostgreSQL と連携します。環境変数の接続文字列を変更し、postgresql プロバイダーを使用するように prisma/schema.prisma を更新します。セッション量が多いアプリの場合は、ホット認証パスのデータベース負荷を軽減するために、セッション ストレージとして PostgreSQL ではなく Redis を検討してください。

Remix で構築された Shopify アプリをデプロイするにはどうすればよいですか?

Shopify では、Node.js ランタイムをサポートするプラットフォーム (Fly.io、Render、Railway、AWS (EC2/ECS)、または Google Cloud Run) にデプロイすることをお勧めします。 Vercel と Netlify はフロントエンドとして機能しますが、Shopify のセッション ストレージに必要な永続的な Node.js サーバーを実行できません。デプロイメント プラットフォームが Webhook 処理とバックグラウンド ジョブの長時間実行プロセスをサポートしていることを確認してください。


次のステップ

認証、GraphQL データ管理、Webhook、請求を正しく処理するカスタム Shopify アプリを構築するには、プラットフォームに関する深い専門知識が必要です。設定が間違っているアプリは、App Store の審査に落ちたり、販売店を破壊したり、セキュリティの脆弱性を露呈したりすることがあります。

ECOSIRE の Shopify アプリ開発サービス は、アーキテクチャ設計、Remix/Polaris 実装、GraphQL API 統合、Webhook インフラストラクチャ、請求設定、App Store への申請、リリース後のサポートなど、開発ライフサイクル全体をカバーしています。

カスタム Shopify アプリの要件について話し合ってください 開発チームと。

E

執筆者

ECOSIRE Research and Development Team

テクニカルライティング

ECOSIRE のテクニカルライティングチームは、Odoo ERP、Shopify による EC、AI エージェント、Power BI による分析、GoHighLevel による自動化、そしてエンタープライズソフトウェアのベストプラクティスを扱っています。私たちのガイドは、企業が十分な情報に基づいてテクノロジーを判断できるよう支援します。

ECOSIRE

Shopify ストアを拡張する

高成長の e コマース向けのカスタム開発、最適化、移行サービス。