git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/microservices-api-gateway" ~/.claude/skills/intense-visions-harness-engineering-microservices-api-gateway && rm -rf "$T"
agents/skills/claude-code/microservices-api-gateway/SKILL.mdMicroservices: API Gateway
Route, aggregate, and secure client requests through an API gateway or BFF pattern.
When to Use
- Multiple clients (web, mobile, 3rd party) need to call multiple backend services
- You want a single entry point for auth, rate limiting, logging, and routing
- Frontend apps need aggregated data from multiple services in one request (BFF)
- You want to decouple clients from backend service URLs and topology
Instructions
Custom API gateway with Express (for BFF pattern):
import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; const app = express(); // --- Cross-cutting concerns — apply to all routes --- // 1. Auth middleware app.use(async (req, res, next) => { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) { res.status(401).json({ error: 'Unauthorized' }); return; } try { req.user = await verifyJWT(token); next(); } catch { res.status(401).json({ error: 'Invalid token' }); } }); // 2. Rate limiting import rateLimit from 'express-rate-limit'; app.use( rateLimit({ windowMs: 60_000, max: 100, standardHeaders: true, keyGenerator: (req) => req.user?.id ?? req.ip, }) ); // 3. Request ID app.use((req, res, next) => { req.id = (req.headers['x-request-id'] as string) ?? crypto.randomUUID(); res.setHeader('X-Request-Id', req.id); next(); }); // --- Routing to backend services --- app.use( '/api/orders', createProxyMiddleware({ target: process.env.ORDER_SERVICE_URL, changeOrigin: true, pathRewrite: { '^/api/orders': '' }, on: { proxyReq: (proxyReq, req) => { proxyReq.setHeader('X-User-Id', req.user!.id); proxyReq.setHeader('X-Request-Id', req.id!); }, error: (err, req, res) => { (res as express.Response).status(503).json({ error: 'Service unavailable' }); }, }, }) ); app.use( '/api/products', createProxyMiddleware({ target: process.env.CATALOG_SERVICE_URL, changeOrigin: true, pathRewrite: { '^/api/products': '' }, }) );
BFF (Backend for Frontend) — aggregation:
// Mobile app needs order + product details in one call // Without BFF: mobile makes 3 requests (order, products × N, user) // With BFF: one call, gateway aggregates app.get('/bff/mobile/order-detail/:orderId', async (req, res) => { const { orderId } = req.params; const userId = req.user!.id; try { // Parallel fetch from backend services const [order, user] = await Promise.all([ orderServiceClient.getOrder(orderId), userServiceClient.getUser(userId), ]); // Verify ownership if (order.userId !== userId) { res.status(403).json({ error: 'Forbidden' }); return; } // Fetch product details for each item in parallel const products = await Promise.all( order.items.map((item) => catalogServiceClient.getProduct(item.productId)) ); // Shape the response for mobile — only what the app needs res.json({ orderId: order.id, status: order.status, createdAt: order.createdAt, customerName: user.name, items: order.items.map((item, i) => ({ name: products[i].name, imageUrl: products[i].imageUrl, quantity: item.quantity, price: item.unitPrice, })), total: order.total, tracking: order.trackingNumber, }); } catch (err) { res.status(500).json({ error: 'Failed to load order' }); } });
Service client with circuit breaker:
import CircuitBreaker from 'opossum'; class OrderServiceClient { private breaker: CircuitBreaker; constructor(private readonly baseUrl: string) { this.breaker = new CircuitBreaker(this.rawFetch.bind(this), { timeout: 5_000, errorThresholdPercentage: 50, resetTimeout: 30_000, }); this.breaker.fallback(() => null); } async getOrder(orderId: string): Promise<Order | null> { return this.breaker.fire(orderId) as Promise<Order | null>; } private async rawFetch(orderId: string): Promise<Order> { const res = await fetch(`${this.baseUrl}/orders/${orderId}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } }
Details
Gateway vs. BFF: An API Gateway handles routing and cross-cutting concerns for all clients. A BFF (Backend for Frontend) is a specialized gateway for one client type (web BFF, mobile BFF), aggregating and shaping responses for that client's specific needs.
What belongs in the gateway:
- Authentication / JWT validation
- Rate limiting and throttling
- Request correlation (IDs, tracing)
- SSL termination
- Routing
What does NOT belong in the gateway:
- Business logic
- Data transformation beyond response shaping for clients
- Database queries
Anti-patterns:
- Gateway that becomes a monolith — it should route, not implement business logic
- Synchronous fan-out to 10+ services per request — use async patterns or pre-materialized views
- No fallback when a downstream service is down — always return partial responses gracefully
Managed options: AWS API Gateway, Kong, Nginx, Traefik, Envoy. Use these for production instead of building your own unless you have very specific BFF aggregation needs.
Source
microservices.io/patterns/apigateway.html
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.