关于 commerce-api-gateway
面向定制化无头电商前端的技能,介绍如何构建 GraphQL Federation 网关或 REST BFF,并在网关统一处理认证、限流、缓存和可观测性。
主要功能
- 01配置 Apollo Router 联邦网关
- 02使用 Fastify 构建 REST BFF
- 03在网关集中处理认证
- 04设置限流与响应缓存
- 05添加 OpenTelemetry 分布式追踪
适用场景
- 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详细介绍
Commerce API 网关
概述
API 网关位于店面客户端与一组后端 Commerce 服务之间,为身份验证、速率限制、缓存和请求路由提供统一入口。在可组合 Commerce 架构中,网关会聚合来自不同服务(目录、购物车、搜索、CMS)的 API,让前端只需发起一次或少数几次调用,而不必发起数十次调用。本技能涵盖使用 Apollo Router 构建 GraphQL Federation 网关、REST 聚合 BFF,以及在网关层处理横切关注点(身份验证、速率限制、可观测性)。
何时使用此技能
- 当你的店面在每次页面加载时需要向不同服务发起 10 次以上 API 调用
- 当你需要在所有 Commerce API 中统一实施身份验证和授权
- 当不同团队分别负责不同服务,而你需要在前端和后端之间建立契约
- 当你希望实施速率限制、熔断或缓存,而不修改每个服务
- 当你需要一个涵盖目录、库存、CMS 和个性化数据的统一 GraphQL 架构
前置条件和平台说明
此技能适用于自定义/无头店面(Node.js、Python 或类似后端)。代码示例使用 TypeScript/Node.js,可适配任何技术栈。
Shopify:Shopify Hydrogen 是 Shopify 的无头框架。使用 Shopify 作为 Commerce 后端并搭配自定义前端,或将 Shopify 与其他同类最佳服务混合使用时,可采用 MACH/可组合模式。 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(面向前端的后端)
对于倾向使用 REST 而非 GraphQL 的店面:
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,并将解码后的身份转发给子图:
// 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 —— 架构拼接虽然可行,但 Federation 才是标准做法;它能明确所有权,并支持在 CI 中进行架构注册表检查
- 为每项服务设置独立超时 —— 响应缓慢的库存服务不应拖慢商品页面;为每个子图设置独立超时,并对非关键数据使用
@defer - 在网关而非客户端缓存 —— 使用
Cache-Control和 Fastify/Apollo 缓存插件,将重复查询的结果从内存中返回;这可以降低所有下游服务的负载 - 为网关 API 而非子图设置版本 —— 在网关层公开
/api/v1和/api/v2前缀;各个子图的架构可在 Federation 下独立演进 - 为每个子图使用健康检查端点 —— 配置路由器轮询每项服务的
/health,并自动将不健康的子图从路由中移除 - 将网关延迟作为系统级 SLO 进行监控 —— 网关的 p99 延迟决定了每次用户交互的延迟上限;当延迟超过页面性能预算时发出告警
常见问题
| 问题 | 解决方案 |
|---|---|
| 解析实体引用时出现 N+1 查询 | 使用 Apollo Federation 的 @key 和批量解析(DataLoader 模式),每项服务通过一次调用获取所有被引用的实体 |
| 身份验证令牌未转发到子图 | 配置 Apollo Router 的 headers.all 传播,将 Authorization 和 x-customer-id 标头转发到所有子图 |
| 网关在高负载下成为瓶颈 | 将 Apollo Router 部署为负载均衡器后方、可水平扩展的无状态服务;它采用 Rust 核心,每个实例可处理数千 RPS |
| 子图架构冲突阻碍部署 | 在 CI 中使用 rover subgraph check,在合并前根据注册表验证架构变更 |
| 来自 SPA 的跨源请求被阻止 | 将 CORS 来源明确设置为你的店面域名;经过身份验证的网关绝不要使用通配符 * |
相关技能
- @composable-commerce
- @webhook-architecture
- @monitoring-alerting-commerce
- @edge-commerce
- @load-testing-commerce