算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
backend-bff-pattern【设计与创意】Use this skill when the user says 'BFF', 'Backend for Frontend', 'API gateway', 'gateway specialization', 'frontend API', 'mobile API', 'web API', 'API composition', 'aggregation service', 'frontend gateway'. This skill designs Backend for Frontend patterns with specialized API gateways for each client type. Applies to any backend stack. Do NOT use f…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Design specialized Backend for Frontend (BFF) services that compose and transform backend APIs for each client type (web, mobile, IoT, third-party), reducing over-fetching and client complexity.
Exact user phrases: "BFF", "Backend for Frontend", "API gateway", "gateway specialization", "frontend API", "mobile API", "web API", "API composition", "aggregation service", "frontend gateway", "BFF pattern".
BFF architecture design or implementation snippet. No file unless requested.
BFF: {client type}
Backing Services: [{service list}]
Composition: {aggregation|transformation|caching}
Security: {auth pattern}
4 lines per BFF. 20 lines for full design.
Web BFF -> SPA (React, Vue)
Mobile BFF -> iOS + Android apps
Partners BFF -> Third-party integrations
Admin BFF -> Internal admin panel
Each BFF exposes APIs shaped for its client:
// Web BFF: returns a page-shaped response
GET /api/web/checkout/{cartId}
{
"items": [
{"id": "prod-1", "name": "Widget", "quantity": 2, "price": 19.99, "image": "https://..."}
],
"subtotal": 39.98,
"shipping": 5.99,
"tax": 3.20,
"total": 49.17,
"shippingOptions": [
{"id": "standard", "name": "Standard", "price": 5.99, "estimate": "5-7 days"},
{"id": "express", "name": "Express", "price": 12.99, "estimate": "2-3 days"}
],
"paymentMethods": [
{"id": "card", "name": "Credit Card", "last4": "4242", "expiry": "04/27"}
]
}
// Mobile BFF: returns a compact response with mobile-specific fields
GET /api/mobile/checkout/{cartId}
{
"total": 49.17,
"shippingOptions": ["standard", "express"],
"defaultPayment": "card_4242"
}
// Partner BFF: returns normalized data with partner-specific metadata
GET /api/partners/checkout/{cartId}?partner=acme
{
"orderReference": "ext-123",
"items": [...],
"commission": 2.46,
"total": 49.17
}
// Web BFF composition logic
class WebCheckoutComposer {
constructor(
private cartService: CartServiceClient,
private pricingService: PricingServiceClient,
private shippingService: ShippingServiceClient,
private paymentService: PaymentServiceClient,
private cache: CacheService,
) {}
async getCheckout(cartId: string, userId: string): Promise<CheckoutResponse> {
const cacheKey = `checkout:web:${cartId}`;
const cached = await this.cache.get(cacheKey);
if (cached) return JSON.parse(cached);
try {
const [cart, pricing, shipping, payment] = await Promise.all([
this.cartService.getCart(cartId),
this.pricingService.calculateTotal(cartId),
this.shippingService.getOptions(cartId, { zipCode: cart.zipCode }),
this.paymentService.getMethods({ userId }),
]);
const response = {
items: cart.items.map(item => ({
id: item.productId,
name: item.name,
quantity: item.quantity,
price: item.unitPrice,
image: item.imageUrl,
})),
subtotal: pricing.subtotal,
shipping: pricing.shipping,
tax: pricing.tax,
total: pricing.total,
shippingOptions: shipping.options.map(opt => ({
id: opt.id,
name: opt.displayName,
price: opt.price,
estimate: opt.deliveryEstimate,
})),
paymentMethods: payment.methods.map(m => ({
id: m.id,
name: m.displayName,
last4: m.lastFourDigits,
expiry: m.expiryDate,
})),
};
await this.cache.set(cacheKey, JSON.stringify(response), { ttl: 30 });
return response;
} catch (error) {
throw new BffError('Failed to compose checkout', {
cartId,
cause: error,
partial: await this.buildPartialResponse(cartId),
});
}
}
private async buildPartialResponse(cartId: string): Promise<Partial<CheckoutResponse>> {
try {
const cart = await this.cartService.getCart(cartId);
return { items: cart.items, subtotal: cart.totalAmount };
} catch {
return {};
}
}
}
| BFF Type | Auth Mechanism | Token Storage | CSRF |
|---|---|---|---|
| Web BFF | Session cookie | httpOnly cookie | SameSite=Strict + CSRF token |
| Mobile BFF | Bearer JWT | Device keystore | Not needed |
| Partner BFF | API Key + HMAC | Server-side | Request signing |
| Admin BFF | SSO + JWT | httpOnly cookie | CSRF token |
// BFF auth middleware factory
function createBffAuth(bffType: 'web' | 'mobile' | 'partner' | 'admin') {
switch (bffType) {
case 'web':
return sessionAuth({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET,
cookie: { httpOnly: true, secure: true, sameSite: 'strict' },
});
case 'mobile':
return jwtAuth({
secret: process.env.MOBILE_JWT_SECRET,
audience: 'mobile-app',
maxAge: '15m',
});
case 'partner':
return apiKeyAuth({
header: 'X-API-Key',
validator: async (key) => partnerService.validateApiKey(key),
});
case 'admin':
return ssoAuth({
issuer: process.env.SSO_ISSUER,
audience: 'admin-panel',
});
}
}
// Caching strategy per BFF
const BFF_CACHE_CONFIG = {
web: {
ttl: 30, // seconds — web pages change frequently
invalidationTags: ['checkout', 'cart'],
staleWhileRevalidate: 300,
},
mobile: {
ttl: 60, // seconds — mobile tolerates slightly stale data
invalidationTags: ['checkout'],
staleWhileRevalidate: 600,
},
partner: {
ttl: 10, // seconds — partners expect fresh data
invalidationTags: ['checkout'],
staleWhileRevalidate: 30,
},
};
// Cache-aware data fetching
async function getCheckoutCached(cartId: string, bffType: string): Promise<CheckoutResponse> {
const cacheConfig = BFF_CACHE_CONFIG[bffType];
const cached = await cache.get(`checkout:${bffType}:${cartId}`);
if (cached && !isStale(cached.timestamp, cacheConfig.ttl)) {
return cached.data;
}
if (cached && cacheConfig.staleWhileRevalidate) {
// Return stale data, refresh in background
refreshCache(cartId, bffType);
return cached.data;
}
const fresh = await composeCheckout(cartId, bffType);
await cache.set(`checkout:${bffType}:${cartId}`, {
data: fresh,
timestamp: Date.now(),
}, { ttl: cacheConfig.staleWhileRevalidate || cacheConfig.ttl });
return fresh;
}
// BFF error handling: partial success, not total failure
class BffService {
async composeWithFallback(serviceCalls: Promise<any>[], fallbacks: any[]): Promise<any> {
const results = await Promise.allSettled(serviceCalls);
return results.map((result, index) => {
if (result.status === 'fulfilled') return result.value;
logger.warn('Service call failed, using fallback', { index, error: result.reason });
return fallbacks[index];
});
}
async getProductPage(productId: string): Promise<ProductPageResponse> {
const [product, inventory, reviews, recommendations] = await this.composeWithFallback(
[
this.productService.getProduct(productId),
this.inventoryService.getStock(productId),
this.reviewService.getReviews(productId),
this.recommendationService.getRelated(productId),
],
[
null, // product: no fallback, page shows error
{ inStock: true, quantity: 0 }, // inventory: optimistic fallback
[], // reviews: empty fallback
[], // recommendations: empty fallback
]
);
if (!product) throw new NotFoundError('Product not found');
return {
product,
inventory: { inStock: inventory.inStock, quantity: inventory.quantity },
reviews,
recommendations,
_warnings: results.some(r => r.status === 'rejected')
? ['Some data temporarily unavailable']
: undefined,
};
}
}
Number of distinct client types?
├── 1-2 → Single BFF with route segregation (/api/web, /api/mobile)
└── 3+ → Dedicated BFF service per client type, separate deployment
Client types differ significantly in:
├── Data shape (page vs compact) → Separate BFFs
├── Auth mechanism (cookie vs token vs API key) → Separate BFFs
├── Performance requirements (latency sensitive?) → Separate BFFs
└── Similar across clients → Single BFF with routes
What does the client need?
├── Raw proxy with auth/rate-limiting → API Gateway (Kong, NGINX, Envoy)
├── Client-defined queries → GraphQL federation
└── Server-shaped responses per client → BFF Pattern
If BFF:
├── Simple aggregation (1-2 services) → Lightweight BFF (single file, simple compose)
├── Complex composition (3+ services) → Full BFF service with circuit breakers, caching
└── Event-driven composition → Async BFF with message queue + SSE/WebSocket push
If API Gateway works but clients need different data:
├── Can gateway transform responses? → Yes → API Gateway with response transformation
└── No → BFF behind the gateway
If GraphQL but clients are diverse:
├── Clients are internal → Single GraphQL gateway is fine
└── Clients are external (3rd party) → BFF wrapping GraphQL (limits query complexity)
Data changes infrequently?
├── Yes → Long TTL (minutes). Cache-aside pattern.
└── No → Short TTL + stale-while-revalidate. Background refresh.
BFF cache location:
├── In-memory (fast, lost on restart) → Best for single-instance, ephemeral data
├── Redis (distributed, consistent) → Best for multi-instance, shared cache
└── CDN (edge caching) → Best for public, static-like data
Cache invalidation method:
├── TTL-based → Simple, but stale window exists
├── Tag-based (purge by category) → Precise, requires event system
└── Webhook (backend notifies BFF) → Real-time, requires backend integration
Deployment model:
├── Monolith with route segregation
│ ├── Pros: Simple, single deploy, shared infra
│ └── Cons: Scales together, one failure domain
│ Best for: Early stage, < 3 clients, small team
├── Per-client BFF service
│ ├── Pros: Independent scaling, deploy, failure isolation
│ └── Cons: More infra, inter-service comms, duplicated caches
│ Best for: Mature product, 3+ clients, dedicated frontend teams
└── BFF per feature domain
├── Pros: Team-aligned (checkout BFF, search BFF), fine-grained scaling
└── Cons: Many services, client calls multiple BFFs, higher complexity
Best for: Large org with domain-aligned teams
Auth pattern:
├── API Gateway handles auth → BFF receives validated user context
├── BFF handles auth directly → Dedicated auth per client type (preferred for security)
└── Backend service handles auth → BFF is passthrough (simplest, least secure)
| Threat | Impact | Mitigation |
|---|---|---|
| BFF as attack vector | Compromised BFF exposes all backend | Least privilege, network isolation |
| Token forwarding | BFF leaks user token to wrong service | Scoped tokens, per-service API keys |
| Cache poisoning | Stale/malicious data served to clients | Signed cache, input validation |
| DoS via composition | One slow service blocks all BFF requests | Per-service timeouts, circuit breakers |
| Data leakage across BFFs | Mobile BFF returns web-only data | Strict type contracts per BFF |
Total BFF latency = max(service1, service2, ..., serviceN) + serialization
Serialization cost: JSON ~0.1ms/KB, Protobuf ~0.01ms/KB
For blocking composition (sequential):
Total = service1 + service2 + ... + serviceN → Death by N * P50
For parallel composition (Promise.all):
Total = max(service1, service2, ..., serviceN) → Only as slow as slowest
// Per-service connection pool with keep-alive
import http from 'http';
const SERVICE_POOLS = {
cart: new http.Agent({ keepAlive: true, maxSockets: 50, timeout: 5000 }),
pricing: new http.Agent({ keepAlive: true, maxSockets: 25, timeout: 10000 }),
shipping: new http.Agent({ keepAlive: true, maxSockets: 25, timeout: 3000 }),
};
async function callService(name: string, url: string): Promise<any> {
const agent = SERVICE_POOLS[name];
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), agent.timeout);
try {
const response = await fetch(url, { agent, signal: controller.signal });
return response.json();
} finally {
clearTimeout(timeout);
}
}
// BFF load shedding — reject early when under pressure
class BffLoadShedder {
private activeRequests = 0;
private maxConcurrent: number;
private queue: Array<{ resolve: Function; reject: Function }> = [];
constructor(maxConcurrent: number = 100) {
this.maxConcurrent = maxConcurrent;
}
async acquire<T>(fn: () => Promise<T>): Promise<T> {
if (this.activeRequests >= this.maxConcurrent) {
// Shed load — return 503 immediately
throw new BffOverloadedError('Too many requests');
}
this.activeRequests++;
try {
return await fn();
} finally {
this.activeRequests--;
}
}
}
// Usage in BFF route
app.get('/api/web/checkout/:cartId', async (req, res) => {
try {
const result = await loadShedder.acquire(() => composer.getCheckout(req.params.cartId, req.user.id));
res.json(result);
} catch (err) {
if (err instanceof BffOverloadedError) {
res.status(503).json({ error: 'Service temporarily unavailable' });
} else {
throw err;
}
}
});
// Structured BFF logging with request tracing
interface BffLogContext {
traceId: string;
bffType: string;
clientIp: string;
userId: string;
duration: number;
services: Array<{ name: string; status: string; duration: number }>;
}
// Export metrics per BFF
// - bff_request_duration_seconds{type="web", route="checkout"}
// - bff_composition_services_count{type="mobile"}
// - bff_cache_hit_ratio{type="partner"}
// - bff_partial_failure_count{type="admin"}
// - bff_downstream_error_count{service="cart", type="web"}
Shared monolithic BFF: One BFF serving web and mobile. Over-fetching for mobile, under-fetching for web. Always separate.
Business logic in BFF: BFF should only compose and transform. Business rules in backend services.
Synchronous dependency chain: BFF calls A, A calls B, B calls C. Latency multiplies. Compose in parallel.
No circuit breaker: Backend service failure cascades. Use circuit breaker pattern with fallback responses.
Caching without invalidation: Stale data served indefinitely. Use tag-based invalidation.
Ignoring partial failures: Single failing service causes entire request failure. Return partial data with warnings.
Auth bypass in BFF: BFF must validate auth before forwarding requests. Never trust the client.
Missing timeout configuration: BFF waits indefinitely for backing service. Set timeouts per service call.
Data transformation in client: BFF should shape data for the client. Client should not transform.
BFF as API gateway: BFF is not Kong, NGINX, or Envoy. It is a service with composition logic.
| Feature | BFF Pattern | GraphQL Federation | API Gateway (Kong) |
|---|---|---|---|
| Data shaping | Server-defined | Client-defined | Proxy only |
| Client specificity | Per-client BFFs | Unified graph | Single API |
| Caching | Application-level | Per-resolver | Edge caching |
| Over-fetching | None (shaped) | Client controlled | High |
| Complexity | Moderate | High | Low |
| Learning curve | Low | High | Moderate |
| Performance | High | Moderate | High |
| Backend coupling | Low (BFF adapts) | High (schema) | Low |
| Tool | Purpose |
|---|---|
| Node.js (Express/Fastify) | BFF runtime |
| Redis | Distributed caching, session store |
| Axios / node-fetch | HTTP client for backing services |
| opossum | Circuit breaker |
| pino | Structured logging with request tracing |
| Docker | Per-BFF containerization |
| Kubernetes | BFF deployment and scaling |
| Prometheus | BFF metrics (latency, error rate) |
| Grafana | BFF monitoring dashboards |
| jaeger/zipkin | Distributed tracing |
No artifact produced unless requested. Next skill: data-masking — protect sensitive data flowing through BFF responses. Carry forward: client types, BFF API contracts, security patterns.