Skip to main content

アプリアーキテクチャ

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 の破壊的変更

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>
);
}
Polaris バージョン

Remix テンプレートには Polaris 12.x+ が同梱されています。アップグレード時は必ず Polaris 移行ガイド を確認してください。メジャーバージョン間でコンポーネント API が変更されます。BlockStackInlineStack コンポーネントは、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 });
};
必須 GDPR Webhook

すべての Shopify アプリは、3つの GDPR Webhook(CUSTOMERS_DATA_REQUESTCUSTOMERS_REDACTSHOP_REDACT)を処理する必要があります。これらが適切に実装されていない場合、App Store レビューに失敗します。アプリが顧客データを保存していない場合でも、これらの Webhook に 200 レスポンスで応答する必要があります。

Webhook のベストプラクティス

  1. 常に 200 を素早く返す -- Webhook ペイロードは非同期で処理してください。Shopify は 5 秒以内のレスポンスを期待しています。
  2. 冪等性を処理する -- Shopify は同じ Webhook を複数回送信する場合があります。X-Shopify-Webhook-Id ヘッダーを使用して重複排除してください。
  3. HMAC 署名を検証する -- authenticate.webhook() 呼び出しがこれを処理しますが、カスタムハンドラーを構築する場合は、常に X-Shopify-Hmac-SHA256 ヘッダーを検証してください。
  4. 配信失敗を監視する -- Shopify は失敗した Webhook を 48 時間再試行します。エンドポイントが一貫して失敗している場合、Shopify は登録を削除します。

アーキテクチャ意思決定図

次のステップ

アーキテクチャの基盤を理解したら、Admin UI エクステンション に進んで、カスタム UI コンポーネントで Shopify Admin を拡張する方法を学びましょう。