⚡ Shopify API連携設定 完全マニュアル
Cloudflare Workers統合・レート制限対策・PII分離設計
CLI/SDK不使用、純粋REST API統合の実装ガイド
🎯 API連携アーキテクチャ概要
システム構成図
graph TB
subgraph "Frontend Applications"
A1[🐱 猫アプリ]
A2[🎨 時の絵]
A3[🛠️ 管理画面]
end
subgraph "Cloudflare Workers BFF"
B1[🔐 Auth Layer]
B2[⚡ Rate Limiter]
B3[🔄 Queue Manager]
B4[📊 API Router]
end
subgraph "Shopify APIs"
C1[🛒 Storefront API<br/>GraphQL]
C2[⚙️ Admin API<br/>REST]
C3[🎣 Webhooks]
end
subgraph "Cloudflare Data"
D1[📊 D1 Database<br/>非PII のみ]
D2[📁 R2 Storage<br/>非PII のみ]
D3[🔄 Workers Queue]
end
A1 --> B1
A2 --> B1
A3 --> B1
B1 --> B4
B4 --> B2
B2 --> B3
B3 --> C1
B3 --> C2
C3 --> B3
B4 --> D1
B4 --> D2
B3 --> D3
style B2 fill:#ffebcd,stroke:#ff6b6b,stroke-width:3px
style C1 fill:#e6f3ff,stroke:#0066cc,stroke-width:2px
style C2 fill:#fff0e6,stroke:#ff6600,stroke-width:2px
🔧 API統合設定
1. Cloudflare Workers環境設定
1.1 環境変数設定
// wrangler.toml
[env.production.vars]
SHOPIFY_STORE_DOMAIN = "contents-print.myshopify.com"
SHOPIFY_ADMIN_API_TOKEN = "shpat_xxxxxxxxxxxxxxxxxxxxxxxx"
SHOPIFY_STOREFRONT_TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
SHOPIFY_API_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
SHOPIFY_WEBHOOK_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
FIREBASE_PROJECT_ID = "contents-print-prod"
[env.development.vars]
SHOPIFY_STORE_DOMAIN = "contents-print-dev.myshopify.com"
SHOPIFY_ADMIN_API_TOKEN = "shpat_dev_xxxxxxxxxxxxxxxxxxxxxxxx"
SHOPIFY_STOREFRONT_TOKEN = "dev_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
1.2 TypeScript型定義
// types/shopify.ts
export interface ShopifyConfig {
storeDomain: string;
adminApiToken: string;
storefrontToken: string;
apiSecret: string;
webhookSecret: string;
apiVersion: string;
}
export interface ShopifyCustomer {
id: string;
email: string;
first_name: string;
last_name: string;
// PII: Shopifyでのみ管理
}
export interface ShopifyOrder {
id: string;
customer_id: string;
line_items: ShopifyLineItem[];
// PII: Shopifyでのみ管理
}
export interface NonPIIOrderData {
firebase_uid: string;
brand: 'neko' | 'tokinoe';
status: string;
created_at: string;
// 非PII: D1で管理可能
}
2. Storefront API統合(商品表示・カート機能)
2.1 商品取得API実装
// api/storefront/products.ts
export class StorefrontAPI {
private config: ShopifyConfig;
constructor(config: ShopifyConfig) {
this.config = config;
}
// 商品一覧取得(ブランド別)
async getProductsByBrand(brand: 'neko' | 'tokinoe', limit: number = 20): Promise<Product[]> {
const query = `
query GetProductsByBrand($first: Int!, $query: String!) {
products(first: $first, query: $query) {
edges {
node {
id
title
handle
description
images(first: 10) {
edges {
node {
originalSrc
altText
}
}
}
variants(first: 10) {
edges {
node {
id
title
price {
amount
}
availableForSale
}
}
}
metafields(identifiers: [
{namespace: "custom", key: "brand"},
{namespace: "custom", key: "firebase_uid"}
]) {
key
value
}
}
}
}
}
`;
const variables = {
first: limit,
query: `metafields.custom.brand:${brand}`
};
return this.graphqlRequest(query, variables);
}
// GraphQLリクエスト実行
private async graphqlRequest(query: string, variables: any) {
const response = await fetch(`https://${this.config.storeDomain}/api/${this.config.apiVersion}/graphql.json`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': this.config.storefrontToken,
},
body: JSON.stringify({ query, variables })
});
if (!response.ok) {
throw new Error(`Storefront API Error: ${response.statusText}`);
}
return response.json();
}
}
2.2 カート管理API実装
// api/storefront/cart.ts
export class CartAPI {
private storefrontAPI: StorefrontAPI;
// カート作成
async createCart(lineItems: CartLineItem[]): Promise<Cart> {
const mutation = `
mutation CartCreate($input: CartInput!) {
cartCreate(input: $input) {
cart {
id
checkoutUrl
estimatedCost {
totalAmount {
amount
}
}
lines(first: 100) {
edges {
node {
id
quantity
merchandise {
... on ProductVariant {
id
title
price {
amount
}
}
}
}
}
}
}
userErrors {
field
message
}
}
}
`;
const variables = {
input: {
lines: lineItems.map(item => ({
merchandiseId: item.variantId,
quantity: item.quantity,
attributes: [
{ key: "firebase_uid", value: item.firebaseUid },
{ key: "brand", value: item.brand }
]
}))
}
};
return this.storefrontAPI.graphqlRequest(mutation, variables);
}
}
3. Admin API統合(注文管理・商品管理)
3.1 レート制限対策システム
// api/rate-limiter.ts
export class RateLimiter {
private queue: Queue;
private maxRequestsPerMinute = 40;
private requestCount = 0;
private windowStart = Date.now();
constructor(queue: Queue) {
this.queue = queue;
}
async executeRequest<T>(requestFn: () => Promise<T>): Promise<T> {
const now = Date.now();
// 1分経過したらカウンタリセット
if (now - this.windowStart >= 60000) {
this.requestCount = 0;
this.windowStart = now;
}
// レート制限チェック
if (this.requestCount >= this.maxRequestsPerMinute) {
// Queueに追加して後で処理
return this.queue.send({
type: 'shopify_api_request',
payload: requestFn.toString(),
timestamp: now
});
}
this.requestCount++;
return requestFn();
}
}
3.2 注文管理API実装
// api/admin/orders.ts
export class AdminOrderAPI {
private config: ShopifyConfig;
private rateLimiter: RateLimiter;
// 注文一覧取得(Firebase UID別)
async getOrdersByFirebaseUID(firebaseUID: string): Promise<Order[]> {
return this.rateLimiter.executeRequest(async () => {
const response = await fetch(
`https://${this.config.storeDomain}/admin/api/${this.config.apiVersion}/orders.json` +
`?metafield[custom.firebase_uid]=${firebaseUID}`,
{
headers: {
'X-Shopify-Access-Token': this.config.adminApiToken,
}
}
);
if (!response.ok) {
throw new Error(`Admin API Error: ${response.statusText}`);
}
const data = await response.json();
// PIIを除去して非PIIデータのみ返す
return data.orders.map(order => this.sanitizeOrder(order));
});
}
// 注文ステータス更新
async updateOrderStatus(orderId: string, status: string): Promise<void> {
return this.rateLimiter.executeRequest(async () => {
const response = await fetch(
`https://${this.config.storeDomain}/admin/api/${this.config.apiVersion}/orders/${orderId}.json`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': this.config.adminApiToken,
},
body: JSON.stringify({
order: {
id: orderId,
tags: status,
metafields: [
{
namespace: "custom",
key: "processing_status",
value: status
}
]
}
})
}
);
if (!response.ok) {
throw new Error(`Failed to update order status: ${response.statusText}`);
}
});
}
// PII除去(重要)
private sanitizeOrder(order: any): NonPIIOrderData {
return {
id: order.id,
firebase_uid: order.metafields?.find(m => m.key === 'firebase_uid')?.value,
brand: order.metafields?.find(m => m.key === 'brand')?.value,
status: order.fulfillment_status || 'pending',
created_at: order.created_at,
line_items_count: order.line_items.length,
total_price: order.total_price,
// PII関連は除外
};
}
}
3.3 顧客管理API実装
// api/admin/customers.ts
export class AdminCustomerAPI {
private config: ShopifyConfig;
private rateLimiter: RateLimiter;
// Firebase UIDとShopify Customer IDの連携
async linkFirebaseUID(customerId: string, firebaseUID: string): Promise<void> {
return this.rateLimiter.executeRequest(async () => {
const response = await fetch(
`https://${this.config.storeDomain}/admin/api/${this.config.apiVersion}/customers/${customerId}/metafields.json`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': this.config.adminApiToken,
},
body: JSON.stringify({
metafield: {
namespace: "custom",
key: "firebase_uid",
value: firebaseUID,
type: "single_line_text_field"
}
})
}
);
if (!response.ok) {
throw new Error(`Failed to link Firebase UID: ${response.statusText}`);
}
});
}
// Firebase UIDから顧客情報取得(非PIIのみ)
async getCustomerByFirebaseUID(firebaseUID: string): Promise<NonPIICustomerData | null> {
return this.rateLimiter.executeRequest(async () => {
const response = await fetch(
`https://${this.config.storeDomain}/admin/api/${this.config.apiVersion}/customers.json` +
`?metafield[custom.firebase_uid]=${firebaseUID}`,
{
headers: {
'X-Shopify-Access-Token': this.config.adminApiToken,
}
}
);
if (!response.ok) {
throw new Error(`Failed to get customer: ${response.statusText}`);
}
const data = await response.json();
if (data.customers.length === 0) {
return null;
}
// PIIを除去
const customer = data.customers[0];
return {
shopify_id: customer.id,
firebase_uid: firebaseUID,
created_at: customer.created_at,
updated_at: customer.updated_at,
orders_count: customer.orders_count,
total_spent: customer.total_spent,
// PII関連は除外(名前、住所、電話番号等)
};
});
}
}
4. Queue System統合(レート制限対策)
4.1 Queue Worker実装
// workers/shopify-queue-worker.ts
export default {
async queue(batch: MessageBatch<ShopifyQueueMessage>, env: Env): Promise<void> {
for (const message of batch.messages) {
try {
await this.processShopifyRequest(message.body, env);
message.ack();
} catch (error) {
console.error('Queue processing error:', error);
message.retry();
}
}
},
async processShopifyRequest(message: ShopifyQueueMessage, env: Env): Promise<void> {
const { type, payload, timestamp, retryCount = 0 } = message;
// 指数バックオフで待機
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
await new Promise(resolve => setTimeout(resolve, delay));
switch (type) {
case 'order_update':
await this.updateOrder(payload, env);
break;
case 'customer_sync':
await this.syncCustomer(payload, env);
break;
case 'product_update':
await this.updateProduct(payload, env);
break;
default:
throw new Error(`Unknown queue message type: ${type}`);
}
},
// レート制限内での実行保証
async updateOrder(payload: any, env: Env): Promise<void> {
const adminAPI = new AdminOrderAPI(env);
// 40req/minを確実に守る
const rateLimitKey = `shopify_rate_limit_${Math.floor(Date.now() / 60000)}`;
const currentCount = await env.KV.get(rateLimitKey) || '0';
if (parseInt(currentCount) >= 39) { // 余裕を持って39
throw new Error('Rate limit reached, will retry');
}
await adminAPI.updateOrderStatus(payload.orderId, payload.status);
await env.KV.put(rateLimitKey, (parseInt(currentCount) + 1).toString(), { expirationTtl: 60 });
}
};
5. Webhook処理実装
5.1 Webhook検証・ルーティング
// api/webhooks/handler.ts
export class WebhookHandler {
private config: ShopifyConfig;
async handleWebhook(request: Request): Promise<Response> {
// 署名検証
if (!await this.verifyWebhook(request)) {
return new Response('Unauthorized', { status: 401 });
}
const topic = request.headers.get('x-shopify-topic');
const body = await request.json();
switch (topic) {
case 'orders/create':
return this.handleOrderCreate(body);
case 'orders/updated':
return this.handleOrderUpdate(body);
case 'orders/paid':
return this.handleOrderPaid(body);
case 'customers/create':
return this.handleCustomerCreate(body);
default:
console.warn(`Unhandled webhook topic: ${topic}`);
return new Response('OK', { status: 200 });
}
}
// 注文作成時の処理
private async handleOrderCreate(order: any): Promise<Response> {
// PIIを含む情報は一時的にのみ使用
const nonPIIData = {
shopify_order_id: order.id,
firebase_uid: order.metafields?.find(m => m.key === 'firebase_uid')?.value,
brand: order.metafields?.find(m => m.key === 'brand')?.value,
status: 'pending',
created_at: order.created_at,
line_items_count: order.line_items.length,
};
// D1に非PIIデータのみ保存
await this.saveToD1(nonPIIData);
// 工場システムに通知(Queueへ)
await this.notifyFactorySystem(nonPIIData);
return new Response('OK', { status: 200 });
}
// Webhook署名検証(セキュリティ重要)
private async verifyWebhook(request: Request): Promise<boolean> {
const signature = request.headers.get('x-shopify-hmac-sha256');
if (!signature) return false;
const body = await request.text();
const hash = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(this.config.webhookSecret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const calculatedSignature = await crypto.subtle.sign(
'HMAC',
hash,
new TextEncoder().encode(body)
);
const base64Signature = btoa(String.fromCharCode(...new Uint8Array(calculatedSignature)));
return signature === base64Signature;
}
}
6. エラーハンドリング・監視
6.1 エラーハンドリング戦略
// utils/error-handler.ts
export class ShopifyErrorHandler {
static async handleAPIError(error: any, context: string): Promise<void> {
const errorInfo = {
context,
error: error.message,
timestamp: new Date().toISOString(),
stack: error.stack
};
// レート制限エラー
if (error.message.includes('429')) {
console.warn('Rate limit hit, queuing request:', errorInfo);
// Queueに追加
return;
}
// 認証エラー
if (error.message.includes('401') || error.message.includes('403')) {
console.error('Authentication error:', errorInfo);
// 管理者にアラート
await this.sendAlert('AUTH_ERROR', errorInfo);
return;
}
// その他のエラー
console.error('Shopify API error:', errorInfo);
await this.logError(errorInfo);
}
static async sendAlert(type: string, data: any): Promise<void> {
// Slackやメール等でアラート送信
// 実装省略
}
}
6.2 監視・メトリクス
// utils/metrics.ts
export class ShopifyMetrics {
static async recordAPICall(endpoint: string, duration: number, success: boolean): Promise<void> {
const metrics = {
endpoint,
duration,
success,
timestamp: Date.now()
};
// CloudflareのAnalyticsやDatadogに送信
await fetch('https://api.datadoghq.com/api/v1/series', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'DD-API-KEY': 'your-api-key'
},
body: JSON.stringify({
series: [{
metric: 'shopify.api.calls',
points: [[Date.now() / 1000, success ? 1 : 0]],
tags: [`endpoint:${endpoint}`]
}]
})
});
}
}
🔒 セキュリティ・PII分離の実装
PII完全分離システム
// utils/pii-filter.ts
export class PIIFilter {
// PIIフィールド一覧
private static readonly PII_FIELDS = [
'email', 'first_name', 'last_name', 'phone',
'address1', 'address2', 'city', 'zip', 'province',
'billing_address', 'shipping_address'
];
// オブジェクトからPII除去
static sanitize<T>(obj: any): Partial<T> {
const sanitized = { ...obj };
for (const field of this.PII_FIELDS) {
if (field in sanitized) {
delete sanitized[field];
}
}
// ネストしたオブジェクトも処理
for (const key in sanitized) {
if (typeof sanitized[key] === 'object' && sanitized[key] !== null) {
sanitized[key] = this.sanitize(sanitized[key]);
}
}
return sanitized;
}
// PII含有チェック
static containsPII(obj: any): boolean {
return this.PII_FIELDS.some(field => field in obj);
}
}
✅ API統合チェックリスト
基本設定
- Cloudflare Workers環境変数設定
- TypeScript型定義作成
- Shopify設定値確認
Storefront API
- 商品取得API実装
- カート管理API実装
- ブランド別フィルタリング実装
Admin API
- レート制限対策実装
- 注文管理API実装
- 顧客管理API実装
Queue System
- Queue Worker実装
- レート制限管理実装
- エラーハンドリング実装
Webhook
- Webhook署名検証実装
- 各種Webhookハンドラー実装
- PII分離処理実装
セキュリティ
- PIIフィルター実装
- エラーハンドリング実装
- 監視・メトリクス実装
📍 次のステップ: Webhook設定 に進んでください。
所要時間: 約6-8時間
難易度: 🔴 最高(複雑な実装多数)
文書作成日: 2025-08-23
最終更新: 2025-08-23
バージョン: 1.0 - API連携特化版