shopify-app-development について
Shopify CLI 3.x の Remix テンプレートを使うアプリ開発者向けのスキルです。埋め込みアプリ、OAuth、Admin API、Polaris UI の構築を扱います。
主な機能
- 01Remix アプリのスキャフォールディング
- 02OAuth とセッション管理
- 03Admin API アクセス
- 04Polaris UI の構築ガイド
- 05Webhook とデプロイのガイド
活用シーン
- 01公開またはカスタム Shopify アプリの開始
- 02マーチャント向け埋め込みアプリの構築
- 03認証済み Admin API 操作の追加
- 04Express ベースの Shopify アプリの刷新
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill shopify-app-developmentインストール後、Claude Code で/shopify-app-developmentを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
tile.jsonevals/embedded-ui-with-polaris-and-app-bridge/criteria.jsontask.mdevals/gdpr-webhooks-and-hmac-validation/criteria.jsontask.mdevals/production-session-storage-and-api-versi/criteria.jsontask.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 スタックに置き換える場合
基本的な手順
-
Shopify CLI でアプリのひな形を作成する
npm install -g @shopify/cli @shopify/theme shopify app init my-shopify-app # Choose: Remix template cd my-shopify-app shopify app devOAuth、セッションストレージ(デフォルトでは SQLite)、App Bridge があらかじめ設定された Remix アプリのひな形が作成されます。開発コマンドは Cloudflare 経由でローカルサーバーをトンネルし、Partner 開発ストアにアプリをインストールします。
-
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; -
ルートを保護し、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}`} />; } -
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> ); } -
スコープを設定し、アプリの再インストールに対応する
必要なスコープを
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 では、これらのエンドポイントが必須です。 -
本番環境にデプロイする
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