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
}
}
}
`);
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 の信頼性に関するベスト プラクティス:
- 200 応答をすぐに返します。操作が遅い場合は、バックグラウンド キューを介して非同期に処理します。
- Webhook の
X-Shopify-Webhook-Idヘッダーを使用して冪等性を実装します。 - HMAC 署名を検証します (
authenticate.webhookによって処理されます) - 再試行ロジックの処理 — Shopify は失敗した Webhook を 48 時間にわたって最大 19 回再試行します
- デバッグのためにすべての 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 アプリの要件について話し合ってください 開発チームと。
執筆者
ECOSIRE Research and Development Team
ECOSIREでエンタープライズグレードのデジタル製品を開発。Odoo統合、eコマース自動化、AI搭載ビジネスソリューションに関するインサイトを共有しています。
関連記事
Case Study: eCommerce Migration to Shopify with Odoo Backend
How a fashion retailer migrated from WooCommerce to Shopify and connected it to Odoo ERP, cutting order fulfillment time by 71% and growing revenue 43%.
Integrating GoHighLevel CRM with eCommerce Stores
Step-by-step guide to integrating GoHighLevel CRM with Shopify and WooCommerce. Sync orders, automate post-purchase flows, and recover abandoned carts at scale.
Odoo + Shopify Sync: Products, Orders, and Inventory
Complete guide to syncing Odoo 19 with Shopify. Covers product sync, real-time order import, bidirectional inventory, financial reconciliation, and multi-store management.