アプリアーキテクチャ
Shopify アプリの構築には、認証とセッション管理から UI レンダリングとイベントハンドリングまで、アプリケーションが Shopify プラットフォームとすべてのレイヤーでどのように統合されるかを理解する必要があります。このレッスンでは、本番品質の Shopify アプリを構築するために必要な基盤アーキテクチャの決定とパターンをカバーします。
アプリタイプ
Shopify は3つの主要なアプリタイプをサポートしており、それぞれ異なるユースケースに適しています:
パブリックアプリ
パブリックアプリは Shopify App Store に掲載され、どのマーチャントでもインストールできます。アプリレビュープロセスを経て、Shopify の品質、セキュリティ、パフォーマンス要件を満たす必要があります。
- Shopify App Store を通じて配布
- 各マーチャントとの OAuth ベースの認証
- マルチテナントのデータ分離を処理する必要がある
- Shopify との収益分配(Shopify は最初の $1M まで 0%、その後はパーセンテージを徴収)
カスタムアプリ
カスタムアプリは単一のマーチャントまたは組織向けに構築されます。App Store には掲載されず、Shopify 管理画面から直接インストールされます。
- 特定のストアまたは Plus 組織向けに構築
- カスタムアプリアクセストークンを使用(OAuth フロー不要)
- 単一テナントのためアーキテクチャがシンプル
- App Store レビュー不要
ドラフトアプリ
ドラフトアプリはレビューに申請されていない開発中のアプリです。テスト用の開発ストアにインストールできます。
- Partners Dashboard または Shopify CLI で作成
- インストール URL 経由で開発ストアにインストール
- テストと開発のためのフル機能
- 準備ができたらパブリックアプリまたはカスタムアプリに変換
特定のクライアント向けに構築する場合は、カスタムアプリから始めてください。エコシステム向けのプロダクトを構築する場合は、ドラフトアプリとして開始し、準備ができたらパブリックに変換してください。このレッスンのアーキテクチャパターンは3つのタイプすべてに適用されます。
Remix アプリテンプレート
Shopify の公式アプリスキャフォールドは、フルスタック React フレームワークである Remix 上に構築されています。テンプレートは必要なものをすべてそのまま提供します:
# Create a new Shopify app with the Remix template
shopify app init --template remix
# Project structure after scaffolding
my-shopify-app/
├── app/
│ ├── routes/
│ │ ├── app._index.tsx # Main app page
│ │ ├── app.tsx # App layout with nav
│ │ ├── auth.$.tsx # OAuth callback handler
│ │ ├── auth.login/
│ │ │ └── route.tsx # Login page
│ │ └── webhooks.tsx # Webhook handler
│ ├── shopify.server.ts # Shopify API configuration
│ └── entry.server.tsx # Server entry point
├── extensions/ # App extensions
├── prisma/
│ └── schema.prisma # Database schema
├── shopify.app.toml # App configuration
├── package.json
└── remix.config.js
主要ファイルの説明
shopify.server.ts ファイルは Shopify 統合の中核です。認証、API アクセス、セッションストレージを設定します:
// app/shopify.server.ts
import "@shopify/shopify-app-remix/adapters/node";
import {
ApiVersion,
AppDistribution,
shopifyApp,
DeliveryMethod,
} from "@shopify/shopify-app-remix/server";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
apiVersion: ApiVersion.January25,
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",
},
PRODUCTS_UPDATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},
hooks: {
afterAuth: async ({ session }) => {
// Register webhooks after successful OAuth
shopify.registerWebhooks({ session });
},
},
});
export default shopify;
export const apiVersion = ApiVersion.January25;
export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
export const authenticate = shopify.authenticate;
export const unauthenticated = shopify.unauthenticated;
export const login = shopify.login;
export const registerWebhooks = shopify.registerWebhooks;
export const sessionStorage = shopify.sessionStorage;
App Bridge 4.x
App Bridge は、アプリのフロントエンドを Shopify 管理画面に接続する JavaScript ライブラリです。バージョン 4.x は統合モデルを簡素化する大幅な刷新です。
App Bridge の機能
- ナビゲーション:アプリは Shopify Admin の iframe 内に表示されます。App Bridge がナビゲーション、戻るボタン、URL の同期を処理します。
- 認証:セッショントークンを管理し、API リクエストが適切に認証されるようにします。
- UI 統合:トースト、モーダル、コンテキストバーなどの Shopify Admin UI 要素へのアクセスを提供します。
- リソースピッカー:マーチャントが商品、コレクション、その他の Shopify リソースを選択するための組み込みコンポーネントです。
Remix での App Bridge
Remix テンプレートでは、App Bridge は自動的に設定されます。app.tsx レイアウトコンポーネントがアプリをラップします:
// app/routes/app.tsx
import { Outlet } from "@remix-run/react";
import { AppProvider } from "@shopify/shopify-app-remix/react";
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
export default function App() {
return (
<AppProvider isEmbeddedApp apiKey={window.ENV.SHOPIFY_API_KEY}>
<Outlet />
</AppProvider>
);
}
App Bridge 4.x では、以前のバージョンで使用されていた createApp() 関数パターンが削除されました。App Bridge 3.x から移行する場合、明示的なアプリインスタンスを作成する必要はありません。代わりに、アプリが Shopify Admin 内でロードされると App Bridge が自動的に初期化されます。window.shopify の直接呼び出しが、以前の app.dispatch() パターンに代わります。
Polaris デザインシステム
Polaris は Shopify のデザインシステムで、Shopify 管理画面のルック&フィールに一致する React コンポーネントを提供します。Polaris を使用することで、アプリがマーチャントにとってネイティブに感じられるようになります。
// app/routes/app._index.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import {
Page,
Layout,
Card,
BlockStack,
Text,
DataTable,
Badge,
Button,
InlineStack,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { admin } = await authenticate.admin(request);
const response = await admin.graphql(`
query {
orders(first: 10, sortKey: CREATED_AT, reverse: true) {
edges {
node {
id
name
totalPriceSet {
shopMoney {
amount
currencyCode
}
}
displayFulfillmentStatus
createdAt
}
}
}
}
`);
const data = await response.json();
return json({ orders: data.data.orders.edges.map((e: any) => e.node) });
};
export default function Index() {
const { orders } = useLoaderData<typeof loader>();
const rows = orders.map((order: any) => [
order.name,
`${order.totalPriceSet.shopMoney.amount} ${order.totalPriceSet.shopMoney.currencyCode}`,
order.displayFulfillmentStatus,
new Date(order.createdAt).toLocaleDateString(),
]);
return (
<Page title="Dashboard" primaryAction={{ content: "Sync Data" }}>
<Layout>
<Layout.Section>
<Card>
<BlockStack gap="300">
<InlineStack align="space-between">
<Text as="h2" variant="headingMd">
Recent Orders
</Text>
<Badge tone="success">Live</Badge>
</InlineStack>
<DataTable
columnContentTypes={["text", "numeric", "text", "text"]}
headings={["Order", "Total", "Fulfillment", "Date"]}
rows={rows}
/>
</BlockStack>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}
Remix テンプレートには Polaris 12.x+ が同梱されています。アップグレード時は必ず Polaris 移行ガイド を確認してください。メジャーバージョン間でコンポーネント API が変更されます。BlockStack と InlineStack コンポーネントは、Polaris 12 で以前の Stack コンポーネントに代わるものです。
セッション管理
Shopify アプリは認証済みマーチャントのセッションを管理する必要があります。セッションにはショップドメイン、アクセストークン、権限スコープが含まれます。Remix テンプレートは Prisma バックドのセッションストレージを通じてこれを処理します。
セッションフロー
セッションストレージ設定
// prisma/schema.prisma
model Session {
id String @id
shop String
state String
isOnline Boolean @default(false)
scope String?
expires DateTime?
accessToken String
userId BigInt?
@@index([shop])
}
Shopify のアクセストークンをログに記録したり、クライアントサイドのコードに公開したり、Cookie に保存したりしないでください。アクセストークンはアプリのスコープ内でマーチャントのストアへの完全な API アクセスを許可します。データベースに暗号化して保存し、サーバーサイドからのみアクセスしてください。
Webhook とイベント駆動アーキテクチャ
Webhook は、マーチャントのストアでイベントが発生したときに Shopify がアプリに送信する HTTP コールバックです。リアクティブなアプリ動作の基盤です。
Webhook の登録
Webhook は shopify.server.ts で宣言され、OAuth 後に自動的に登録されます:
// In shopify.server.ts webhooks config
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
ORDERS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
PRODUCTS_UPDATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
CUSTOMERS_DATA_REQUEST: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
CUSTOMERS_REDACT: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
SHOP_REDACT: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},
Webhook の処理
// app/routes/webhooks.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import db from "../db.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { topic, shop, session, admin, payload } =
await authenticate.webhook(request);
switch (topic) {
case "APP_UNINSTALLED":
if (session) {
await db.session.deleteMany({ where: { shop } });
}
// Clean up app data for this shop
await db.appData.deleteMany({ where: { shop } });
break;
case "ORDERS_CREATE":
// Process new order
await processNewOrder(shop, payload);
break;
case "PRODUCTS_UPDATE":
// Sync product changes
await syncProductUpdate(shop, payload);
break;
case "CUSTOMERS_DATA_REQUEST":
// GDPR: Return customer data
await handleDataRequest(shop, payload);
break;
case "CUSTOMERS_REDACT":
// GDPR: Delete customer data
await handleCustomerRedact(shop, payload);
break;
case "SHOP_REDACT":
// GDPR: Delete all shop data
await handleShopRedact(shop, payload);
break;
default:
console.warn(`Unhandled webhook topic: ${topic}`);
}
return new Response(null, { status: 200 });
};
すべての Shopify アプリは、3つの GDPR Webhook(CUSTOMERS_DATA_REQUEST、CUSTOMERS_REDACT、SHOP_REDACT)を処理する必要があります。これらが適切に実装されていない場合、App Store レビューに失敗します。アプリが顧客データを保存していない場合でも、これらの Webhook に 200 レスポンスで応答する必要があります。
Webhook のベストプラクティス
- 常に 200 を素早く返す -- Webhook ペイロードは非同期で処理してください。Shopify は 5 秒以内のレスポンスを期待しています。
- 冪等性を処理する -- Shopify は同じ Webhook を複数回送信する場合があります。
X-Shopify-Webhook-Idヘッダーを使用して重複排除してください。 - HMAC 署名を検証する --
authenticate.webhook()呼び出しがこれを処理しますが、カスタムハンドラーを構築する場合は、常にX-Shopify-Hmac-SHA256ヘッダーを検証してください。 - 配信失敗を監視する -- Shopify は失敗した Webhook を 48 時間再試行します。エンドポイントが一貫して失敗している場合、Shopify は登録を削除します。
アーキテクチャ意思決定図
アーキテクチャの基盤を理解したら、Admin UI エクステンション に進んで、カスタム UI コンポーネントで Shopify Admin を拡張する方法を学びましょう。