;
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
interface RawCatalog {
standard: { inventory: Record<string, any>[]; transactions: Record<string, any>[] };
european: { inventory: Record<string, any>[]; transactions: Record<string, any>[] };
}
const rawSource: RawCatalog = JSON.parse(
readFileSync(join(__dirname, 'warehouse.json'), 'utf-8')
);
const app = express();
app.use(express.json());
const resolveDataset = (locale: string): RawCatalog['standard'] | RawCatalog['european'] => {
return locale === 'eu' ? rawSource.european : rawSource.standard;
};
app.get('/v1/raw/inventory', (req: Request, res: Response) => {
const locale = req.headers['x-client-locale'] as string || 'standard';
const dataset = resolveDataset(locale);
// Simulate I/O latency
setTimeout(() => {
res.json({ payload: dataset.inventory });
}, 160);
});
app.get('/v1/raw/transactions', (req: Request, res: Response) => {
const locale = req.headers['x-client-locale'] as string || 'standard';
const dataset = resolveDataset(locale);
setTimeout(() => {
res.json({ payload: dataset.transactions });
}, 190);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(Data layer active on :${PORT}));
**Architecture Rationale:** The handler no longer maps fields, calculates prices, or constructs client-facing shapes. It reads a header, selects a dataset, and returns it. This eliminates transformation drift and ensures the backend remains stable regardless of client requirements.
### Step 2: Define Gateway Policy
The gateway now owns two responsibilities: routing requests to the correct backend path based on headers, and transforming raw payloads into a consistent client contract. This is implemented as a declarative pipeline.
```typescript
// Gateway transformation pipeline (TypeScript)
interface GatewayContext {
request: { headers: Record<string, string> };
response: { body: any };
}
const regionRouter = (ctx: GatewayContext): string => {
const locale = ctx.request.headers['x-client-locale'] || 'standard';
return locale === 'eu' ? '/v1/raw/inventory' : '/v1/raw/inventory';
};
const inventoryTransformer = (raw: any[]): any[] => {
return raw.map((item) => ({
identifier: item.sku_code,
name: item.item_title,
category: item.classification,
pricing: {
amount: item.unit_cost_cents / 100,
currency: item.fiat_code,
},
stock: item.quantity_on_hand,
metadata: {
dimensions: item.size_specifications,
weight: item.mass_grams,
tags: item.labels,
},
}));
};
const transactionTransformer = (raw: any[]): any[] => {
return raw.map((tx) => ({
reference: tx.order_ref,
state: tx.current_status,
total: (tx.amount_cents / 100).toFixed(2),
timestamp: tx.created_at,
buyer: tx.purchaser_details,
fulfillment: {
method: tx.shipping_type,
address: tx.delivery_location,
},
payment: tx.settlement_method,
lineItems: tx.purchased_goods,
}));
};
export { regionRouter, inventoryTransformer, transactionTransformer };
Architecture Rationale: Transformation logic is extracted into pure functions. This enables unit testing without spinning up an Express server, guarantees consistent output shapes, and allows the gateway to apply transformations conditionally based on client version or region.
Step 3: Wire the Execution Path
The gateway intercepts incoming requests, applies the routing policy, forwards to the backend, runs the transformation pipeline, and returns the final response. The backend remains unaware of client contracts.
// Gateway execution handler
import { inventoryTransformer, transactionTransformer, regionRouter } from './policy.js';
const handleClientRequest = async (req: Request, res: Response) => {
const targetPath = regionRouter({ request: req, response: { body: null } });
try {
const upstream = await fetch(`http://backend:3000${targetPath}`, {
headers: { 'x-client-locale': req.headers['x-client-locale'] || 'standard' },
});
if (!upstream.ok) {
return res.status(502).json({ error: 'Data layer unreachable', code: 'UPSTREAM_FAIL' });
}
const raw = await upstream.json();
const isInventory = targetPath.includes('inventory');
const transformed = isInventory
? inventoryTransformer(raw.payload)
: transactionTransformer(raw.payload);
res.json(isInventory ? { catalog: transformed } : { ledger: transformed });
} catch (err) {
console.error('Gateway execution failed', err);
res.status(500).json({ error: 'Policy execution error', code: 'GATEWAY_ERR' });
}
};
Architecture Rationale: The gateway acts as a policy engine. It handles upstream communication, error normalization, and response shaping. The backend scales independently. Client contracts are enforced at a single choke point, eliminating handler-level inconsistency.
Pitfall Guide
Explanation: Applying complex business rules, aggregations, or multi-source joins inside the gateway creates hidden latency and breaks idempotency.
Fix: Restrict gateway transformations to field mapping, type coercion, and shape normalization. Keep aggregations in the data layer or a dedicated compute service.
Explanation: Gateways often drop custom headers for security or compliance. If x-client-locale is stripped before reaching the backend, routing defaults incorrectly.
Fix: Explicitly configure header forwarding rules. Validate header presence in gateway logs before upstream calls.
3. Bypassing the Gateway in Development
Explanation: Developers calling the backend directly during local testing miss transformation bugs and routing mismatches.
Fix: Run a local gateway proxy in dev environments. Enforce all traffic through the policy layer using Docker Compose or local DNS overrides.
4. Ignoring Type Safety Across the Boundary
Explanation: Raw backend data and transformed client shapes often drift apart, causing runtime type errors in downstream consumers.
Fix: Maintain a shared OpenAPI or JSON Schema document. Generate TypeScript interfaces for both raw and transformed payloads. Validate transformations against the schema in CI.
Explanation: Caching gateway responses without accounting for transformation parameters (e.g., region, client version) serves stale or mismatched data.
Fix: Include transformation keys in cache identifiers. Example: cache-key: {path}:{locale}:{client-version}:{hash(transform)}.
6. Hardcoding Region Logic
Explanation: Embedding region checks directly in gateway code creates maintenance debt when new locales are added.
Fix: Externalize routing tables to configuration files or feature flag systems. Use dynamic dispatch based on header values rather than conditional branches.
7. Missing Fallback Chains
Explanation: When the gateway transformation fails, returning a raw backend payload exposes internal field names and breaks client contracts.
Fix: Implement graceful degradation. Return a standardized error envelope with a contract_violation flag. Never leak untransformed upstream data to clients.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single client, stable schema | Backend transformation | Lower infrastructure overhead, simpler deployment | Low |
| Multiple clients, diverging formats | Gateway-offloaded | Centralized contract enforcement, independent scaling | Medium |
| High traffic, region-specific data | Gateway routing + backend caching | Reduces backend load, isolates region logic | Medium-High |
| Strict compliance/audit requirements | Gateway policy layer | Centralized logging, consistent error shapes, easier auditing | Medium |
Configuration Template
# gateway-policy.yaml (Zuplo-compatible structure)
routes:
- path: /api/v1/catalog
methods: [GET]
upstream:
url: http://backend:3000/v1/raw/inventory
headers:
x-client-locale: ${request.headers.x-client-locale}
transformation:
pipeline:
- name: region-router
type: header-dispatch
target: x-client-locale
- name: shape-normalizer
type: field-mapping
schema: ./schemas/inventory-transform.json
error-handling:
upstream-failure:
status: 502
body: { error: "Data layer unreachable", code: "UPSTREAM_FAIL" }
transformation-failure:
status: 500
body: { error: "Contract violation", code: "TRANSFORM_ERR" }
caching:
enabled: true
ttl: 300
key-template: "catalog:{x-client-locale}:{v1}"
- path: /api/v1/ledger
methods: [GET]
upstream:
url: http://backend:3000/v1/raw/transactions
headers:
x-client-locale: ${request.headers.x-client-locale}
transformation:
pipeline:
- name: region-router
type: header-dispatch
target: x-client-locale
- name: shape-normalizer
type: field-mapping
schema: ./schemas/transaction-transform.json
error-handling:
upstream-failure:
status: 502
body: { error: "Data layer unreachable", code: "UPSTREAM_FAIL" }
transformation-failure:
status: 500
body: { error: "Contract violation", code: "TRANSFORM_ERR" }
caching:
enabled: true
ttl: 120
key-template: "ledger:{x-client-locale}:{v1}"
Quick Start Guide
- Isolate the backend: Remove all transformation and routing logic from Express handlers. Return raw datasets only.
- Define the contract: Create JSON Schema or OpenAPI definitions for client-facing response shapes.
- Deploy gateway policy: Upload the routing and transformation configuration to your API gateway. Verify header forwarding rules.
- Validate end-to-end: Run integration tests against the gateway endpoint. Confirm transformation accuracy, error handling, and cache behavior.
- Monitor drift: Enable contract validation in CI. Alert on schema mismatches between raw backend data and transformed client responses.