commerce-api-gateway について
カスタムのヘッドレスコマースストアフロントを構築するチーム向けのスキルです。GraphQL Federation ゲートウェイや REST BFF の構築に加え、認証などの共通処理をゲートウェイに集約する方法を扱います。
主な機能
- 01Apollo Router のフェデレーションゲートウェイ設定
- 02Fastify による REST BFF の構築
- 03ゲートウェイでの認証処理
- 04レート制限とレスポンスキャッシュの設定
- 05OpenTelemetry による分散トレース
活用シーン
- 01カタログ、在庫、CMS の API 集約
- 02コマース API の認証確認の統一
- 03ページ読み込み時のバックエンド呼び出し削減
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill commerce-api-gatewayインストール後、Claude Code で/commerce-api-gatewayを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
tile.jsonevals/apollo-federation-schema-and-router-conf/criteria.jsontask.mdevals/observability-dataloader-and-api-version/criteria.jsontask.mdevals/rest-bff-with-rate-limiting-and-caching/criteria.jsontask.md詳細
コマース API ゲートウェイ
概要
API ゲートウェイは、ストアフロントのクライアントと複数のバックエンドコマースサービスの間に位置し、認証、レート制限、キャッシュ、リクエストルーティングのための単一のエントリーポイントを提供します。コンポーザブルコマースアーキテクチャでは、ゲートウェイが異なるサービス(カタログ、カート、検索、CMS)の API を集約するため、フロントエンドは数十回ではなく、1 回または数回の呼び出しで済みます。このスキルでは、Apollo Router を使用した GraphQL Federation ゲートウェイ、REST 集約 BFF の構築、およびゲートウェイ層での横断的関心事(認証、レート制限、可観測性)の適用について説明します。
このスキルを使用する場面
- ストアフロントがページの読み込みごとに、異なるサービスへ 10 回以上の API 呼び出しを行う場合
- すべてのコマース API 全体で認証と認可を一貫して適用する必要がある場合
- 異なるチームがそれぞれ異なるサービスを担当しており、フロントエンドとバックエンド間の契約が必要な場合
- 各サービスを変更せずに、レート制限、サーキットブレーカー、またはキャッシュを適用したい場合
- カタログ、在庫、CMS、パーソナライゼーションのデータを網羅する単一の GraphQL スキーマが必要な場合
前提条件とプラットフォームに関する注意事項
このスキルは、カスタム/ヘッドレスのストアフロント(Node.js、Python、または同様のバックエンド)を対象としています。コード例では TypeScript/Node.js を使用していますが、あらゆるスタックに応用できます。
Shopify:Shopify Hydrogen は Shopify のヘッドレスフレームワークです。MACH/コンポーザブルパターンは、カスタムフロントエンドのコマースバックエンドとして Shopify を使用する場合や、Shopify を他の各分野で最適なサービスと組み合わせる場合に適用できます。 WooCommerce:WooCommerce は、REST API と WPGraphQL を介してヘッドレスバックエンドとして機能します。これらのパターンは、フロントエンドを WordPress から切り離す場合に適用できます。 Magento:Magento の GraphQL API と PWA Studio はヘッドレスアーキテクチャをサポートしています。これらのコンポーザブルパターンは、MACH スタックのバックエンドサービスとして Magento を使用する場合に適用できます。
必要なもの:
- Node.js 18+(またはバックエンドの言語に合わせて応用してください)
- キャッシュ/キュー用の Redis
基本的な手順
-
GraphQL Federation 用に Apollo Router をセットアップする
Apollo Federation は、複数の GraphQL サブグラフを統合されたスーパグラフに構成します。各サービスはスキーマの一部を所有します。
# Install Apollo Router (the high-performance Rust gateway) curl -sSL https://router.apollo.dev/download/nix/latest | sh # Install Rover CLI for schema management npm install -g @apollo/roverスーパグラフの設定を定義します:
# supergraph.yaml federation_version: =2.5.0 subgraphs: catalog: routing_url: http://catalog-service:4001/graphql schema: subgraph_url: http://catalog-service:4001/graphql inventory: routing_url: http://inventory-service:4002/graphql schema: subgraph_url: http://inventory-service:4002/graphql cart: routing_url: http://cart-service:4003/graphql schema: subgraph_url: http://cart-service:4003/graphql構成して実行します:
rover supergraph compose --config supergraph.yaml > supergraph.graphql ./router --supergraph supergraph.graphql --config router.yaml -
Federation 対応のサブグラフスキーマを定義する
各サービスはスキーマの一部を定義し、他のサービスが所有する型を拡張できます:
# catalog-service/schema.graphql type Query { product(id: ID!): Product products(first: Int, after: String): ProductConnection } type Product @key(fields: "id") { id: ID! name: String! slug: String! description: String price: Money! images: [Image!]! } # inventory-service/schema.graphql — extends Product from catalog type Product @key(fields: "id") @extends { id: ID! @external inventory: InventoryStatus! } type InventoryStatus { available: Boolean! quantity: Int warehouseLocations: [String!]! }ゲートウェイは、カタログのレスポンスから取得した商品 ID を使って在庫サービスを呼び出し、
Product.inventoryフィールドを解決します。これはクライアント側でのオーケストレーションなしに、自動的に行われます。 -
Fastify で REST BFF(フロントエンド向けバックエンド)を構築する
GraphQL より REST を優先するストアフロント向け:
import Fastify from 'fastify'; import {catalogClient} from './services/catalog'; import {inventoryClient} from './services/inventory'; import {cmsClient} from './services/cms'; const app = Fastify({logger: true}); // Composite endpoint for Product Detail Page app.get<{Params: {id: string}}>('/api/pdp/:id', async (request, reply) => { const {id} = request.params; const customerId = request.headers['x-customer-id'] as string | undefined; const [product, inventory, content] = await Promise.allSettled([ catalogClient.getProduct(id), inventoryClient.getStock(id), cmsClient.getProductContent(id), ]); if (product.status === 'rejected') { return reply.status(404).send({error: 'Product not found'}); } return { product: product.value, inventory: inventory.status === 'fulfilled' ? inventory.value : {available: true, quantity: null}, content: content.status === 'fulfilled' ? content.value : null, }; }); // Composite endpoint for Cart Page app.get<{Params: {cartId: string}}>('/api/cart/:cartId', { preHandler: [requireAuth], }, async (request, reply) => { const cart = await cartClient.getCart(request.params.cartId); const productIds = cart.lines.map((l: any) => l.productId); const inventoryMap = await inventoryClient.getBulkStock(productIds); return { ...cart, lines: cart.lines.map((line: any) => ({ ...line, inventory: inventoryMap[line.productId] ?? {available: true}, })), }; }); await app.listen({port: 3000, host: '0.0.0.0'}); -
ゲートウェイで認証を適用する
ゲートウェイは JWT を検証し、デコードした ID 情報をサブグラフに転送します:
// middleware/auth.ts import {FastifyRequest, FastifyReply} from 'fastify'; import {verify, JwtPayload} from 'jsonwebtoken'; export async function requireAuth(request: FastifyRequest, reply: FastifyReply) { const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) return reply.status(401).send({error: 'Authentication required'}); try { const payload = verify(token, process.env.JWT_PUBLIC_KEY!, {algorithms: ['RS256']}) as JwtPayload; request.user = {id: payload.sub!, email: payload.email, roles: payload.roles ?? []}; } catch { return reply.status(401).send({error: 'Invalid token'}); } } // Apollo Router auth via coprocessor (Rust plugin alternative) // router.yaml# router.yaml authentication: router: jwt: jwks: - url: https://your-auth-provider/.well-known/jwks.json authorization: require_authentication: false # Allow public queries; subgraphs enforce per-field auth -
レート制限とレスポンスキャッシュを実装する
// Rate limiting with Redis token bucket import {RateLimiterRedis} from 'rate-limiter-flexible'; import Redis from 'ioredis'; const redis = new Redis(process.env.REDIS_URL!); const rateLimiter = new RateLimiterRedis({ storeClient: redis, keyPrefix: 'rl_gateway', points: 100, // 100 requests duration: 60, // per 60 seconds blockDuration: 60, // block for 60s when exceeded }); app.addHook('preHandler', async (request, reply) => { const key = request.user?.id ?? request.ip; try { await rateLimiter.consume(key); } catch { reply.header('Retry-After', '60'); return reply.status(429).send({error: 'Too many requests'}); } }); // Response caching with stale-while-revalidate import {fastifyCaching} from '@fastify/caching'; app.register(fastifyCaching, {privacy: fastifyCaching.privacy.PUBLIC, expiresIn: 60}); -
OpenTelemetry で分散トレーシングを追加する
// tracing.ts — initialize before importing app modules import {NodeSDK} from '@opentelemetry/sdk-node'; import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-http'; import {HttpInstrumentation} from '@opentelemetry/instrumentation-http'; const sdk = new NodeSDK({ serviceName: 'commerce-gateway', traceExporter: new OTLPTraceExporter({url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT}), instrumentations: [new HttpInstrumentation()], }); sdk.start(); // Propagate trace context to downstream services // OpenTelemetry auto-injects W3C traceparent headers into all outgoing HTTP requests
例
キャッシュと CORS を設定した Apollo Router
# router.yaml
server:
listen: 0.0.0.0:4000
cors:
origins:
- https://www.mystore.com
- https://staging.mystore.com
supergraph:
listen: 0.0.0.0:4000
traffic_shaping:
router:
timeout: 30s
all:
timeout: 10s
retry:
enabled: true
min_per_sec: 10
ttl: 10s
retry_on_http_statuses: [500, 502, 503]
telemetry:
tracing:
otlp:
endpoint: http://otel-collector:4317
protocol: grpc
レガシー REST サービス向けのスキーマスティッチング
import {wrapSchema, RenameTypes, FilterTypes} from '@graphql-tools/wrap';
import {fetch} from 'undici';
// Wrap a legacy REST API as a virtual GraphQL subgraph
const legacyProductSchema = buildSchema(`
type Query {
legacyProduct(sku: String!): LegacyProduct
}
type LegacyProduct {
sku: String!
name: String!
wholesalePrice: Float!
}
`);
const legacyProductSubschema = {
schema: legacyProductSchema,
executor: async ({document, variables}: any) => {
const sku = variables.sku;
const res = await fetch(`${process.env.LEGACY_API_URL}/products/${sku}`);
const product = await res.json();
return {data: {legacyProduct: product}};
},
};
ベストプラクティス
- ゲートウェイは薄く保つ — ゲートウェイは横断的な関心事(認証、レート制限、キャッシュ、トレーシング)を処理し、ビジネスロジックはサービス側に配置します
- GraphQL には Apollo Federation を使用する — スキーマステッチングも機能しますが、標準はフェデレーションです。明確な責任範囲が定められ、CI でのスキーマレジストリチェックが可能になります
- サービスごとにタイムアウトを設定する — インベントリーサービスの遅延によって商品ページ全体が遅れないようにします。各サブグラフに個別のタイムアウトを設定し、重要度の低いデータには
@deferを使用します - クライアントではなくゲートウェイでキャッシュする —
Cache-Controlと Fastify/Apollo のキャッシュプラグインを使って、繰り返しのクエリをメモリから返します。これにより、すべての下流サービスの負荷を軽減できます - サブグラフではなくゲートウェイ API をバージョン管理する — ゲートウェイレベルで
/api/v1と/api/v2のプレフィックスを公開します。各サブグラフのスキーマは、フェデレーションのもとで個別に進化します - 各サブグラフにヘルスチェックエンドポイントを使用する — ルーターが各サービスの
/healthをポーリングし、正常でないサブグラフを自動的にルーティングから除外するよう設定します - ゲートウェイのレイテンシをシステムレベルの SLO として監視する — ゲートウェイの p99 レイテンシが、あらゆるユーザー操作の上限となります。ページのパフォーマンス予算を超えたらアラートを出します
よくある問題
| 問題 | 解決策 |
|---|---|
| エンティティ参照の解決時に N+1 クエリが発生する | Apollo Federation の @key とバッチ解決(DataLoader パターン)を使用し、サービスごとに 1 回の呼び出しですべての参照先エンティティを取得します |
| 認証トークンがサブグラフに転送されない | Authorization ヘッダーと x-customer-id ヘッダーをすべてのサブグラフに転送するよう、Apollo Router の headers.all の伝播を設定します |
| 負荷がかかるとゲートウェイがボトルネックになる | Apollo Router を、ロードバランサーの背後にある水平方向にスケール可能なステートレスサービスとしてデプロイします。Rust コアを備え、インスタンスごとに数千 RPS を処理できます |
| サブグラフのスキーマ競合によってデプロイがブロックされる | マージ前に、CI で rover subgraph check を使用してレジストリに対するスキーマ変更を検証します |
| SPA からのクロスオリジンリクエストがブロックされる | CORS のオリジンをストアフロントのドメインに明示的に設定します。認証が必要なゲートウェイでは、ワイルドカード * を決して使用しないでください |
関連スキル
- @composable-commerce
- @webhook-architecture
- @monitoring-alerting-commerce
- @edge-commerce
- @load-testing-commerce