Building Custom Shopify Apps with Remix and Polaris

Complete developer guide to building custom Shopify apps using Remix, Shopify CLI 3, Polaris design system, and App Bridge for embedded admin experiences.

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

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

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
      }
    }
  }
`);

currentBulkOperationstatus === "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統合、eコマース自動化、AI搭載ビジネスソリューションに関するインサイトを共有しています。

WhatsAppでチャット