mcpField
C
commerce-api-gateway

commerce-api-gateway

github.com/finsilabs開発6072 回インストール5 日前に更新
リポジトリ

commerce-api-gateway について

カスタムのヘッドレスコマースストアフロントを構築するチーム向けのスキルです。GraphQL Federation ゲートウェイや REST BFF の構築に加え、認証などの共通処理をゲートウェイに集約する方法を扱います。

主な機能

  1. 01Apollo Router のフェデレーションゲートウェイ設定
  2. 02Fastify による REST BFF の構築
  3. 03ゲートウェイでの認証処理
  4. 04レート制限とレスポンスキャッシュの設定
  5. 05OpenTelemetry による分散トレース

活用シーン

  1. 01カタログ、在庫、CMS の API 集約
  2. 02コマース API の認証確認の統一
  3. 03ページ読み込み時のバックエンド呼び出し削減

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill commerce-api-gateway

インストール後、Claude Code で/commerce-api-gatewayを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

tile.json
evals/apollo-federation-schema-and-router-conf/
criteria.json
task.md
evals/observability-dataloader-and-api-version/
criteria.json
task.md
evals/rest-bff-with-rate-limiting-and-caching/
criteria.json
task.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

基本的な手順

  1. 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
    
  2. 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 フィールドを解決します。これはクライアント側でのオーケストレーションなしに、自動的に行われます。

  3. 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'});
    
  4. ゲートウェイで認証を適用する

    ゲートウェイは 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
    
  5. レート制限とレスポンスキャッシュを実装する

    // 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});
    
  6. 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

コメント