Commerce API Gateway
Overview
An API gateway sits between storefront clients and the set of backend commerce services, providing a single entry point for authentication, rate limiting, caching, and request routing. In composable commerce architectures, the gateway aggregates APIs from disparate services (catalog, cart, search, CMS) so the frontend makes one or a few calls rather than dozens. This skill covers building a GraphQL Federation gateway with Apollo Router, a REST aggregation BFF, and applying cross-cutting concerns (auth, rate limiting, observability) at the gateway layer.
When to Use This Skill
- When your storefront makes 10+ API calls per page load from different services
- When you need to enforce authentication and authorization consistently across all commerce APIs
- When different teams own different services and you need a contract between the frontend and backend
- When you want to apply rate limiting, circuit breakers, or caching without modifying each service
- When you need a single GraphQL schema that spans catalog, inventory, CMS, and personalization data
Prerequisites & Platform Notes
This skill is written for custom/headless storefronts (Node.js, Python, or similar backend). The code examples use TypeScript/Node.js and can be adapted to any stack.
Shopify: Shopify Hydrogen is Shopify's headless framework. MACH/composable patterns apply when using Shopify as the commerce backend with a custom frontend, or when mixing Shopify with other best-of-breed services.
WooCommerce: WooCommerce can serve as a headless backend via its REST API and WPGraphQL. These patterns apply when decoupling the frontend from WordPress.
Magento: Magento's GraphQL API and PWA Studio support headless architectures. These composable patterns apply to Magento as a backend service in a MACH stack.
You'll need:
- Node.js 18+ (or adapt to your backend language)
- Redis for caching/queues
Core Instructions
Set up Apollo Router for GraphQL Federation
Apollo Federation composes multiple GraphQL subgraphs into a unified supergraph. Each service owns a slice of the schema.
# 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
Define the supergraph config:
# 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
Compose and run:
rover supergraph compose --config supergraph.yaml > supergraph.graphql
./router --supergraph supergraph.graphql --config router.yaml
Define federated subgraph schemas
Each service defines its slice of the schema and can extend types owned by other services:
# 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!]!
}
The gateway resolves the Product.inventory field by calling the inventory service with the product IDs gathered from the catalog response — automatically, without any client-side orchestration.
Build a REST BFF (Backend-for-Frontend) with Fastify
For storefronts that prefer REST over 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'});
Apply authentication at the gateway
The gateway validates JWTs and forwards the decoded identity to subgraphs:
// 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
Implement rate limiting and response caching
// 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});
Add distributed tracing with 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
Examples
Apollo Router configuration with caching and CORS
# 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
Schema stitching for legacy REST services
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}};
},
};
Best Practices
- Keep the gateway thin — the gateway handles cross-cutting concerns (auth, rate limiting, caching, tracing); business logic belongs in the services
- Use Apollo Federation for GraphQL — schema stitching works but federation is the standard; it enforces clear ownership and enables schema registry checks in CI
- Set per-service timeouts — a slow inventory service should not hold up a product page; set independent timeouts for each subgraph and use
@defer for non-critical data
- Cache at the gateway, not the client — use
Cache-Control and Fastify/Apollo caching plugins to serve repeated queries from memory; this reduces load on all downstream services
- Version the gateway API, not the subgraphs — expose
/api/v1 and /api/v2 prefixes at the gateway level; individual subgraph schemas evolve independently under federation
- Use health check endpoints for each subgraph — configure the router to poll
/health on each service and remove unhealthy subgraphs from routing automatically
- Monitor gateway latency as a system-level SLO — the gateway p99 latency is the ceiling on every user interaction; alert when it exceeds your page performance budget
Common Pitfalls
| Problem |
Solution |
| N+1 queries when resolving entity references |
Use Apollo Federation's @key and batch-resolving (DataLoader pattern) to fetch all referenced entities in one call per service |
| Auth token not forwarded to subgraphs |
Configure Apollo Router's headers.all propagation to forward Authorization and x-customer-id headers to all subgraphs |
| Gateway becomes a bottleneck under load |
Deploy Apollo Router as a horizontally scaled stateless service behind a load balancer; it has a Rust core and handles thousands of RPS per instance |
| Subgraph schema conflict blocks deployment |
Use rover subgraph check in CI to validate schema changes against the registry before merging |
| Cross-origin requests blocked from the SPA |
Set CORS origins explicitly to your storefront domains; never use wildcard * on an authenticated gateway |
Related Skills
- @composable-commerce
- @webhook-architecture
- @monitoring-alerting-commerce
- @edge-commerce
- @load-testing-commerce
1---2name: commerce-api-gateway3description: Aggregate multiple commerce microservices behind a single API gateway with GraphQL federation, rate limiting, and unified authentication4---56# Commerce API Gateway78## Overview910An API gateway sits between storefront clients and the set of backend commerce services, providing a single entry point for authentication, rate limiting, caching, and request routing. In composable commerce architectures, the gateway aggregates APIs from disparate services (catalog, cart, search, CMS) so the frontend makes one or a few calls rather than dozens. This skill covers building a GraphQL Federation gateway with Apollo Router, a REST aggregation BFF, and applying cross-cutting concerns (auth, rate limiting, observability) at the gateway layer.1112## When to Use This Skill1314- When your storefront makes 10+ API calls per page load from different services15- When you need to enforce authentication and authorization consistently across all commerce APIs16- When different teams own different services and you need a contract between the frontend and backend17- When you want to apply rate limiting, circuit breakers, or caching without modifying each service18- When you need a single GraphQL schema that spans catalog, inventory, CMS, and personalization data1920## Prerequisites & Platform Notes2122**This skill is written for custom/headless storefronts** (Node.js, Python, or similar backend). The code examples use TypeScript/Node.js and can be adapted to any stack.2324**Shopify**: Shopify Hydrogen is Shopify's headless framework. MACH/composable patterns apply when using Shopify as the commerce backend with a custom frontend, or when mixing Shopify with other best-of-breed services.25**WooCommerce**: WooCommerce can serve as a headless backend via its REST API and WPGraphQL. These patterns apply when decoupling the frontend from WordPress.26**Magento**: Magento's GraphQL API and PWA Studio support headless architectures. These composable patterns apply to Magento as a backend service in a MACH stack.2728**You'll need**:29- Node.js 18+ (or adapt to your backend language)30- Redis for caching/queues3132## Core Instructions33341. **Set up Apollo Router for GraphQL Federation**3536 Apollo Federation composes multiple GraphQL subgraphs into a unified supergraph. Each service owns a slice of the schema.3738 ```bash39 # Install Apollo Router (the high-performance Rust gateway)40 curl -sSL https://router.apollo.dev/download/nix/latest | sh4142 # Install Rover CLI for schema management43 npm install -g @apollo/rover44 ```4546 Define the supergraph config:47 ```yaml48 # supergraph.yaml49 federation_version: =2.5.050 subgraphs:51 catalog:52 routing_url: http://catalog-service:4001/graphql53 schema:54 subgraph_url: http://catalog-service:4001/graphql55 inventory:56 routing_url: http://inventory-service:4002/graphql57 schema:58 subgraph_url: http://inventory-service:4002/graphql59 cart:60 routing_url: http://cart-service:4003/graphql61 schema:62 subgraph_url: http://cart-service:4003/graphql63 ```6465 Compose and run:66 ```bash67 rover supergraph compose --config supergraph.yaml > supergraph.graphql68 ./router --supergraph supergraph.graphql --config router.yaml69 ```70712. **Define federated subgraph schemas**7273 Each service defines its slice of the schema and can extend types owned by other services:7475 ```graphql76 # catalog-service/schema.graphql77 type Query {78 product(id: ID!): Product79 products(first: Int, after: String): ProductConnection80 }8182 type Product @key(fields: "id") {83 id: ID!84 name: String!85 slug: String!86 description: String87 price: Money!88 images: [Image!]!89 }9091 # inventory-service/schema.graphql — extends Product from catalog92 type Product @key(fields: "id") @extends {93 id: ID! @external94 inventory: InventoryStatus!95 }9697 type InventoryStatus {98 available: Boolean!99 quantity: Int100 warehouseLocations: [String!]!101 }102 ```103104 The gateway resolves the `Product.inventory` field by calling the inventory service with the product IDs gathered from the catalog response — automatically, without any client-side orchestration.1051063. **Build a REST BFF (Backend-for-Frontend) with Fastify**107108 For storefronts that prefer REST over GraphQL:109110 ```typescript111 import Fastify from 'fastify';112 import {catalogClient} from './services/catalog';113 import {inventoryClient} from './services/inventory';114 import {cmsClient} from './services/cms';115116 const app = Fastify({logger: true});117118 // Composite endpoint for Product Detail Page119 app.get<{Params: {id: string}}>('/api/pdp/:id', async (request, reply) => {120 const {id} = request.params;121 const customerId = request.headers['x-customer-id'] as string | undefined;122123 const [product, inventory, content] = await Promise.allSettled([124 catalogClient.getProduct(id),125 inventoryClient.getStock(id),126 cmsClient.getProductContent(id),127 ]);128129 if (product.status === 'rejected') {130 return reply.status(404).send({error: 'Product not found'});131 }132133 return {134 product: product.value,135 inventory: inventory.status === 'fulfilled' ? inventory.value : {available: true, quantity: null},136 content: content.status === 'fulfilled' ? content.value : null,137 };138 });139140 // Composite endpoint for Cart Page141 app.get<{Params: {cartId: string}}>('/api/cart/:cartId', {142 preHandler: [requireAuth],143 }, async (request, reply) => {144 const cart = await cartClient.getCart(request.params.cartId);145 const productIds = cart.lines.map((l: any) => l.productId);146 const inventoryMap = await inventoryClient.getBulkStock(productIds);147148 return {149 ...cart,150 lines: cart.lines.map((line: any) => ({151 ...line,152 inventory: inventoryMap[line.productId] ?? {available: true},153 })),154 };155 });156157 await app.listen({port: 3000, host: '0.0.0.0'});158 ```1591604. **Apply authentication at the gateway**161162 The gateway validates JWTs and forwards the decoded identity to subgraphs:163164 ```typescript165 // middleware/auth.ts166 import {FastifyRequest, FastifyReply} from 'fastify';167 import {verify, JwtPayload} from 'jsonwebtoken';168169 export async function requireAuth(request: FastifyRequest, reply: FastifyReply) {170 const token = request.headers.authorization?.replace('Bearer ', '');171 if (!token) return reply.status(401).send({error: 'Authentication required'});172173 try {174 const payload = verify(token, process.env.JWT_PUBLIC_KEY!, {algorithms: ['RS256']}) as JwtPayload;175 request.user = {id: payload.sub!, email: payload.email, roles: payload.roles ?? []};176 } catch {177 return reply.status(401).send({error: 'Invalid token'});178 }179 }180181 // Apollo Router auth via coprocessor (Rust plugin alternative)182 // router.yaml183 ```184185 ```yaml186 # router.yaml187 authentication:188 router:189 jwt:190 jwks:191 - url: https://your-auth-provider/.well-known/jwks.json192193 authorization:194 require_authentication: false # Allow public queries; subgraphs enforce per-field auth195 ```1961975. **Implement rate limiting and response caching**198199 ```typescript200 // Rate limiting with Redis token bucket201 import {RateLimiterRedis} from 'rate-limiter-flexible';202 import Redis from 'ioredis';203204 const redis = new Redis(process.env.REDIS_URL!);205206 const rateLimiter = new RateLimiterRedis({207 storeClient: redis,208 keyPrefix: 'rl_gateway',209 points: 100, // 100 requests210 duration: 60, // per 60 seconds211 blockDuration: 60, // block for 60s when exceeded212 });213214 app.addHook('preHandler', async (request, reply) => {215 const key = request.user?.id ?? request.ip;216 try {217 await rateLimiter.consume(key);218 } catch {219 reply.header('Retry-After', '60');220 return reply.status(429).send({error: 'Too many requests'});221 }222 });223224 // Response caching with stale-while-revalidate225 import {fastifyCaching} from '@fastify/caching';226 app.register(fastifyCaching, {privacy: fastifyCaching.privacy.PUBLIC, expiresIn: 60});227 ```2282296. **Add distributed tracing with OpenTelemetry**230231 ```typescript232 // tracing.ts — initialize before importing app modules233 import {NodeSDK} from '@opentelemetry/sdk-node';234 import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-http';235 import {HttpInstrumentation} from '@opentelemetry/instrumentation-http';236237 const sdk = new NodeSDK({238 serviceName: 'commerce-gateway',239 traceExporter: new OTLPTraceExporter({url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT}),240 instrumentations: [new HttpInstrumentation()],241 });242 sdk.start();243244 // Propagate trace context to downstream services245 // OpenTelemetry auto-injects W3C traceparent headers into all outgoing HTTP requests246 ```247248## Examples249250### Apollo Router configuration with caching and CORS251252```yaml253# router.yaml254server:255 listen: 0.0.0.0:4000256 cors:257 origins:258 - https://www.mystore.com259 - https://staging.mystore.com260261supergraph:262 listen: 0.0.0.0:4000263264traffic_shaping:265 router:266 timeout: 30s267 all:268 timeout: 10s269 retry:270 enabled: true271 min_per_sec: 10272 ttl: 10s273 retry_on_http_statuses: [500, 502, 503]274275telemetry:276 tracing:277 otlp:278 endpoint: http://otel-collector:4317279 protocol: grpc280```281282### Schema stitching for legacy REST services283284```typescript285import {wrapSchema, RenameTypes, FilterTypes} from '@graphql-tools/wrap';286import {fetch} from 'undici';287288// Wrap a legacy REST API as a virtual GraphQL subgraph289const legacyProductSchema = buildSchema(`290 type Query {291 legacyProduct(sku: String!): LegacyProduct292 }293 type LegacyProduct {294 sku: String!295 name: String!296 wholesalePrice: Float!297 }298`);299300const legacyProductSubschema = {301 schema: legacyProductSchema,302 executor: async ({document, variables}: any) => {303 const sku = variables.sku;304 const res = await fetch(`${process.env.LEGACY_API_URL}/products/${sku}`);305 const product = await res.json();306 return {data: {legacyProduct: product}};307 },308};309```310311## Best Practices312313- **Keep the gateway thin** — the gateway handles cross-cutting concerns (auth, rate limiting, caching, tracing); business logic belongs in the services314- **Use Apollo Federation for GraphQL** — schema stitching works but federation is the standard; it enforces clear ownership and enables schema registry checks in CI315- **Set per-service timeouts** — a slow inventory service should not hold up a product page; set independent timeouts for each subgraph and use `@defer` for non-critical data316- **Cache at the gateway, not the client** — use `Cache-Control` and Fastify/Apollo caching plugins to serve repeated queries from memory; this reduces load on all downstream services317- **Version the gateway API, not the subgraphs** — expose `/api/v1` and `/api/v2` prefixes at the gateway level; individual subgraph schemas evolve independently under federation318- **Use health check endpoints for each subgraph** — configure the router to poll `/health` on each service and remove unhealthy subgraphs from routing automatically319- **Monitor gateway latency as a system-level SLO** — the gateway p99 latency is the ceiling on every user interaction; alert when it exceeds your page performance budget320321## Common Pitfalls322323| Problem | Solution |324|---------|----------|325| N+1 queries when resolving entity references | Use Apollo Federation's `@key` and batch-resolving (DataLoader pattern) to fetch all referenced entities in one call per service |326| Auth token not forwarded to subgraphs | Configure Apollo Router's `headers.all` propagation to forward `Authorization` and `x-customer-id` headers to all subgraphs |327| Gateway becomes a bottleneck under load | Deploy Apollo Router as a horizontally scaled stateless service behind a load balancer; it has a Rust core and handles thousands of RPS per instance |328| Subgraph schema conflict blocks deployment | Use `rover subgraph check` in CI to validate schema changes against the registry before merging |329| Cross-origin requests blocked from the SPA | Set `CORS` origins explicitly to your storefront domains; never use wildcard `*` on an authenticated gateway |330331## Related Skills332333- @composable-commerce334- @webhook-architecture335- @monitoring-alerting-commerce336- @edge-commerce337- @load-testing-commerce