Key Points
- AWS API Gateway comes in two flavors: HTTP API (cheaper, faster, 90% of use cases) and REST API (full feature set, higher cost). Use HTTP API unless you need REST API's request/response transformation or private integrations.
- Azure API Management (APIM) is the only managed gateway with a built-in developer portal and a policy engine powerful enough to handle enterprise API governance without custom code.
- GCP offers three overlapping products: API Gateway (serverless, OpenAPI-driven), Cloud Endpoints (gRPC and self-managed), and Apigee (enterprise-grade). Don't pick randomly — they are not interchangeable.
- Common patterns — BFF (Backend for Frontend), API aggregation, and protocol translation — are cloud-agnostic in design but differ significantly in implementation cost across providers.
- Cold starts on Lambda-backed gateways, APIM's XML policy syntax, and GCP's per-project quota model are the three gotchas that will cost you time in production.
Introduction
Picking an API gateway is a three-to-five year decision. Migrating between providers requires rewriting auth flows, rate-limiting config, and deployment pipelines — not just updating a URL. I've evaluated all three in production environments — migrating a client from AWS REST API to HTTP API, standing up APIM for an enterprise API program, and routing Cloud Run services through GCP API Gateway — and the differences that matter are rarely the ones in the marketing comparison tables. Most teams pick the gateway that matches their existing cloud provider without comparing what they're getting.
That's often the right call. But it helps to know what you're trading. AWS, Azure, and GCP have different design philosophies: AWS optimizes for serverless-native integration, Azure optimizes for enterprise API governance, and GCP gives you a choice between a lightweight managed service and a full API platform.
This article explains what each gateway does architecturally, compares them on the features that matter in production, covers the three patterns you'll implement on any of them, and flags the gotchas that each provider's documentation glosses over.
What an API Gateway Does
Before comparing providers, align on the job a gateway performs. An API gateway sits between clients and backend services and handles cross-cutting concerns so your services don't have to.
Diagram: Client traffic enters the gateway, which handles auth and routing before forwarding to backend services.
The five jobs a gateway performs:
- Routing — match request paths to backend services, including path rewriting and load balancing.
- Authentication and authorization — validate JWT tokens, API keys, or OAuth flows before traffic reaches your service.
- Rate limiting — enforce per-client or per-route request quotas; protect backends from traffic spikes.
- Request/response transformation — rewrite headers, translate between protocols (REST ↔ gRPC), strip or inject fields.
- Observability — emit access logs, request metrics, and distributed traces without instrumenting each service.
Every provider covers these five jobs. How they expose them — and at what cost — is what differentiates them.
Request Flow Through a Gateway
Before choosing a provider, understand the sequence every request traverses. The auth, rate-limit, and routing steps happen in this order regardless of which gateway you use.
Diagram: Request flow through a gateway — auth and rate limiting happen before the backend ever receives the request.
AWS API Gateway
AWS offers three API products under the API Gateway brand: HTTP API, REST API, and WebSocket API. HTTP API and REST API are the ones backend engineers choose between daily.
HTTP API vs REST API
| Feature | HTTP API | REST API |
|---|---|---|
| Price | $1.00 / million requests | $3.50 / million requests |
| Latency overhead | ~6 ms | ~11 ms |
| JWT authorizer (native) | Yes | No — Lambda authorizer required |
| Request/response transforms | No | Yes (mapping templates) |
| Usage plans / API keys | No | Yes |
| Private integrations (VPC Link) | Yes | Yes |
| WebSocket | No — separate product | No |
| AWS WAF integration | Yes | Yes |
Use HTTP API for Lambda-backed services, JWT-authenticated endpoints, and any new API where you don't need request/response mapping templates. It costs 71% less than REST API and imposes half the latency overhead. I default to HTTP API for every new AWS project — the only time I've reached for REST API in the last two years was when a client needed usage plans for billing third-party API consumers.
Use REST API when you need usage plans and API keys for third-party developer access, request/response transformation via mapping templates, or Cognito user pool authorizers without a Lambda function.
Lambda Integration
The typical AWS pattern connects API Gateway directly to Lambda. The gateway acts as the event source; Lambda handles the business logic.
// AWS Lambda handler — receives API Gateway proxy event
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
export async function handler(
event: APIGatewayProxyEventV2,
): Promise<APIGatewayProxyResultV2> {
const userId = event.pathParameters?.userId;
if (!userId) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/problem+json' },
body: JSON.stringify({
type: 'https://api.example.com/errors/400',
title: 'Bad Request',
status: 400,
detail: 'userId path parameter is required',
}),
};
}
// Business logic here
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, status: 'active' }),
};
}
# HTTP API route configuration (AWS SAM / CloudFormation)
Resources:
GetUserFunction:
Type: AWS::Serverless::Function
Properties:
Handler: dist/handler.handler
Runtime: nodejs22.x
Events:
GetUser:
Type: HttpApi
Properties:
Path: /users/{userId}
Method: GET
Auth:
Authorizer: JWTAuthorizer
MyHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
Auth:
Authorizers:
JWTAuthorizer:
JwtConfiguration:
Audience: ["https://api.example.com"]
Issuer: "https://auth.example.com/"
IdentitySource: "$request.header.Authorization"
The Cold Start Gotcha
Lambda-backed HTTP APIs incur cold start latency when a function instance is not warm. For a Node.js 22.x Lambda with a small bundle, cold starts add 200–800 ms. For a Java or .NET Lambda with a heavy runtime, expect 1–5 seconds.
Mitigations:
- Enable Provisioned Concurrency on the Lambda — eliminates cold starts for pre-warmed instances, billed hourly even when idle.
- Keep Lambda bundle size under 5 MB (smaller = faster cold start).
- Use SnapStart for Java Lambda functions (zero cold start after initial activation).
Cold starts do not affect AWS ECS or EKS backends connected via VPC Link — they only affect Lambda integrations. I've seen this burn teams who built their entire p99 latency budget around gateway benchmarks, then went live and got paged at 3 AM because a burst of new connections spiked p99 to 2.8 seconds — the Lambda was a .NET 6 function with a 40 MB bundle and no Provisioned Concurrency.
Azure API Management (APIM)
Azure APIM is architecturally different from AWS API Gateway. It is not a simple proxy — it is a full API management platform with three components:
Diagram: APIM's three-component architecture — the gateway, the management plane, and the developer portal are separate concerns.
The Policy Engine
APIM's differentiating feature is its XML-based policy engine. Policies apply at four scopes: global, product, API, and operation. They execute in order: inbound → backend → outbound → on-error.
<!-- APIM policy: validate JWT, rate-limit, inject backend header -->
<policies>
<inbound>
<base />
<!-- Validate JWT from Authorization header -->
<validate-jwt header-name="Authorization" failed-validation-httpcode="401"
failed-validation-error-message="Unauthorized">
<openid-config url="https://auth.example.com/.well-known/openid-configuration" />
<audiences>
<audience>https://api.example.com</audience>
</audiences>
</validate-jwt>
<!-- Rate limit: 100 calls per 60 seconds per subscription key -->
<rate-limit-by-key calls="100" renewal-period="60"
counter-key="@(context.Subscription.Id)" />
<!-- Inject caller identity into backend header -->
<set-header name="X-User-Id" exists-action="override">
<value>@(context.Request.Headers.GetValueOrDefault("Authorization","")
.Replace("Bearer ",""))</value>
</set-header>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- Strip internal headers before returning to client -->
<set-header name="X-Internal-Trace-Id" exists-action="delete" />
</outbound>
<on-error>
<base />
</on-error>
</policies>
The policy engine handles authentication, rate limiting, caching, transformation, and error handling — without custom code. For an enterprise that manages 50+ APIs across multiple teams, that centralization is the primary value proposition.
Developer Portal
APIM includes a fully customizable developer portal out of the box. Developers browse APIs, subscribe to products, generate API keys, and test endpoints — all without involving the API team. AWS and GCP's native gateways require third-party tooling (e.g., Backstage, Stoplight) to achieve the same.
Pricing Tiers
| Tier | Price (approx.) | Notes |
|---|---|---|
| Consumption | $3.50 / million calls | Pay-per-use. No SLA for developer portal. |
| Developer | ~$50/month | Dev/test only. No production SLA. |
| Basic | ~$150/month | Up to 1M calls/month included. |
| Standard | ~$750/month | Full features, 99.9% SLA. |
| Premium | ~$3,800+/month | Multi-region, VNET integration, 99.95% SLA. |
The Consumption tier looks attractive but lacks the developer portal SLA and several advanced policy features. Standard is the minimum for production workloads with enterprise requirements. In practice, I recommend clients skip Consumption entirely for anything customer-facing — the cost delta between Consumption and Standard is irrelevant next to the engineering hours you'll spend working around its limitations.
GCP: API Gateway, Cloud Endpoints, and Apigee
GCP's three API products have overlapping names and confusingly similar descriptions. Choose the wrong one and you'll hit feature walls or pay for enterprise features you don't need.
| Product | Best for | OpenAPI | gRPC | Developer portal | Pricing model |
|---|---|---|---|---|---|
| API Gateway | Serverless backends (Cloud Run, Functions) | Yes (OAS v2) | No | No | Per-call |
| Cloud Endpoints | gRPC services, self-managed gateway | Yes | Yes | No | Per-call |
| Apigee | Enterprise, partner APIs, monetization | Yes | Yes | Yes | Subscription |
Use API Gateway when your backend runs on Cloud Run, Cloud Functions, or App Engine and you want a fully managed gateway with no infrastructure to operate. Import your OpenAPI spec and Google provisions the gateway.
Use Cloud Endpoints when your services speak gRPC, or when you need to host the gateway proxy (ESPv2) on your own runtime for private networking. Cloud Endpoints is also the right choice for local development with gRPC tooling.
Use Apigee when you need a developer portal, API monetization, advanced bot detection, or multi-cloud/hybrid deployment. Apigee is the only GCP option that competes directly with Azure APIM.
GCP API Gateway — OpenAPI-Driven Configuration
# openapi.yaml — GCP API Gateway configuration
swagger: "2.0"
info:
title: User Service API
version: "1.0"
host: api.example.com
schemes:
- https
produces:
- application/json
x-google-backend:
address: https://user-service-xyz-uc.a.run.app
deadline: 30.0
securityDefinitions:
firebase:
authorizationUrl: ""
flow: implicit
type: oauth2
x-google-issuer: "https://securetoken.google.com/my-project"
x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com"
x-google-audiences: "my-project"
paths:
/users/{userId}:
get:
summary: Get user by ID
operationId: getUser
parameters:
- name: userId
in: path
required: true
type: string
security:
- firebase: []
responses:
"200":
description: User found
"401":
description: Unauthorized
"404":
description: Not found
# Deploy to GCP API Gateway
gcloud api-gateway api-configs create user-service-v1 \
--api=user-service \
--openapi-spec=openapi.yaml \
--project=my-project \
--backend-auth-service-account=api-gateway-sa@my-project.iam.gserviceaccount.com
gcloud api-gateway gateways create user-service-gateway \
--api=user-service \
--api-config=user-service-v1 \
--location=us-central1 \
--project=my-project
The GCP Quota Gotcha
GCP API Gateway enforces quotas at the project level. If you run multiple APIs or environments in the same GCP project, they share quota limits. A traffic spike on one API eats into the quota headroom for every other API in that project. I've seen this catch teams when a batch job saturated API quotas during an off-hours data migration, which cascaded into 429s on the customer-facing API living in the same project — a production incident that had nothing to do with the customer-facing service itself.
Structure your GCP projects to isolate production APIs. One project per environment (dev, staging, prod) is the minimum. One project per service in production is safer for high-traffic APIs.
Feature Comparison
| Feature | AWS HTTP API | AWS REST API | Azure APIM (Standard) | GCP API Gateway | GCP Apigee |
|---|---|---|---|---|---|
| Auth: JWT | Native | Lambda authorizer | Policy (validate-jwt) | OpenAPI extension | Policy |
| Auth: API keys | No | Yes | Yes | Yes | Yes |
| Auth: OAuth 2.0 | Partial (JWT) | Lambda authorizer | Policy | No | Yes |
| Rate limiting | Yes | Yes (usage plans) | Yes (per subscription) | Yes (per consumer) | Yes (advanced) |
| Request transform | No | Yes (mapping templates) | Yes (policy engine) | No | Yes |
| Response transform | No | Yes | Yes | No | Yes |
| Protocol translation | No | No | Yes (REST ↔ SOAP) | No | Yes |
| Developer portal | No | No | Yes (built-in) | No | Yes |
| gRPC support | No | No | No | Via Endpoints | Yes |
| WebSocket | Separate product | No | No | No | No |
| Multi-region active-active | No (regional) | No | Premium tier | No | Yes |
| Pricing model | Per-call | Per-call | Tier (monthly) | Per-call | Subscription |
| Cold starts | Lambda-dependent | Lambda-dependent | None | None | None |
Common Patterns
Backend for Frontend (BFF)
The BFF pattern uses a dedicated gateway layer per client type — one for the web app, one for the mobile app — to avoid forcing different clients to negotiate a single general-purpose API.
Diagram: BFF pattern — each client type gets a dedicated aggregation layer tailored to its data needs.
A BFF does data aggregation and transformation — it is not where business logic lives. Business rules belong in the downstream services.
On AWS, implement each BFF as a separate Lambda function or ECS service behind its own HTTP API route. On Azure, use APIM products to route mobile vs web clients to different backends. On GCP, deploy separate Cloud Run services and separate API Gateway configurations.
API Aggregation
Aggregation collapses multiple downstream service calls into a single response for the client. The gateway (or a BFF) fans out the requests in parallel and merges the results.
// API aggregation pattern — call multiple services in parallel
import express, { Request, Response } from 'express';
const app = express();
interface OrderSummary {
order: unknown;
inventory: unknown;
shipping: unknown;
}
/**
* GET /orders/:id/summary — returns a merged view of order, inventory, and shipping data.
*
* @remarks
* Fans out to three downstream services in parallel. If any service fails,
* the endpoint returns 502 with the name of the failing service.
*
* @returns Merged order summary or 502 with failing service name.
*/
app.get('/orders/:id/summary', async (req: Request, res: Response) => {
const { id } = req.params;
const [orderRes, inventoryRes, shippingRes] = await Promise.allSettled([
fetch(`http://order-service/orders/${id}`),
fetch(`http://inventory-service/stock/${id}`),
fetch(`http://shipping-service/shipments/${id}`),
]);
for (const [name, result] of [
['order', orderRes],
['inventory', inventoryRes],
['shipping', shippingRes],
] as const) {
if (result.status === 'rejected') {
res.status(502).json({ error: `upstream_failure`, service: name });
return;
}
}
const summary: OrderSummary = {
order: await (orderRes as PromiseFulfilledResult<Response>).value.json(),
inventory: await (inventoryRes as PromiseFulfilledResult<Response>).value.json(),
shipping: await (shippingRes as PromiseFulfilledResult<Response>).value.json(),
};
res.json(summary);
});
app.listen(3000);
Protocol Translation
Protocol translation converts between API protocols at the gateway layer. The most common case in enterprise environments is REST-to-SOAP, where a new REST API sits in front of a legacy SOAP backend.
Azure APIM handles this natively with the soap-to-rest policy. On AWS or GCP, you write a Lambda or Cloud Run adapter that performs the translation.
<!-- Azure APIM: expose a REST endpoint backed by a SOAP service -->
<policies>
<inbound>
<base />
<!-- Convert REST JSON body to SOAP XML envelope -->
<set-body>
@{
var body = context.Request.Body.As<JObject>();
return string.Format(
@"<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
xmlns:usr='http://legacy.example.com/user'>
<soapenv:Body>
<usr:GetUser>
<usr:UserId>{0}</usr:UserId>
</usr:GetUser>
</soapenv:Body>
</soapenv:Envelope>",
body["userId"]
);
}
</set-body>
<set-header name="Content-Type" exists-action="override">
<value>text/xml; charset=utf-8</value>
</set-header>
<set-header name="SOAPAction" exists-action="override">
<value>http://legacy.example.com/user/GetUser</value>
</set-header>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- Parse SOAP response and return JSON -->
<xml-to-json kind="direct" apply="always" consider-accept-header="false" />
</outbound>
</policies>
Protocol translation is one of the strongest arguments for Azure APIM. Implementing the same pattern on AWS requires a Lambda adapter and custom XML parsing logic. If your organization has legacy SOAP services that need a REST facade, I would pick Azure APIM over any other provider specifically for this — the AWS alternative is a Lambda that becomes a maintenance liability the moment the original SOAP developer leaves.
API Lifecycle Management
Every gateway manages API versions moving through environments. The state diagram below represents the lifecycle that applies regardless of provider.
Diagram: API lifecycle from development through retirement. Breaking changes are only safe before the API reaches Production.
On AWS, implement environment promotion by deploying to separate API Gateway stages (dev, staging, prod). On Azure, use APIM's revision system for non-breaking changes and a new API version for breaking changes. On GCP API Gateway, deploy to separate gateway instances per environment.
Choosing the Right Gateway
Diagram: Decision flowchart for selecting a gateway.
Common Mistakes
Mistake 1: Using AWS REST API when HTTP API covers your needs
REST API costs 3.5× more per million requests and adds ~5 ms of latency over HTTP API. The only reasons to choose REST API over HTTP API are mapping templates for request/response transformation, usage plans with API keys, or Cognito user pool authorizers. If none of those apply, HTTP API is the correct choice.
Mistake 2: Ignoring Lambda cold starts in latency budgets
Teams measure gateway latency and see 6 ms. They assume their API responds in under 100 ms. Then they deploy to production and discover p99 latency is 700 ms because the Lambda behind the gateway cold-starts on every new burst. Cold starts are not a gateway problem — they are a Lambda problem — but they manifest as gateway latency and are often debugged at the wrong layer.
Mistake 3: Choosing Azure APIM Consumption for production without reading the tier limitations
The Consumption tier has no SLA for the developer portal, limits on policy features, and no built-in cache. Teams select it for cost reasons and hit limitations mid-project. Standard is the correct production tier for most workloads.
Mistake 4: Mixing multiple APIs in a single GCP project
GCP API Gateway enforces quotas at the project level. Multiple APIs in one project share quota. A traffic spike on one API reduces the available quota for all others in the same project. Separate projects per environment (and per service in production for high-traffic APIs) is the safe default.
Mistake 5: Building complex API aggregation in gateway mapping templates
AWS REST API mapping templates (Velocity Template Language) and Azure APIM policies can both perform data transformation. Use them for simple field renaming or header injection. Do not use them to aggregate multiple backend calls, implement business logic, or transform complex nested structures — that complexity belongs in a BFF service where it is testable and maintainable.
Production Considerations
Performance
- AWS HTTP API adds ~6 ms of median gateway overhead. REST API adds ~11 ms. Set your latency budget accordingly.
- Azure APIM Standard tier median overhead is 5–15 ms depending on policy complexity. A policy chain with JWT validation, rate limiting, and transformation adds up.
- GCP API Gateway overhead is 5–20 ms. Apigee overhead is higher due to the full policy engine.
Security
- On AWS, attach a WAF WebACL to your HTTP API or REST API. The gateway itself validates JWT signatures but does not inspect payloads for SQL injection or XSS patterns.
- On Azure APIM, use the
ip-filterpolicy to block known-bad IP ranges and thevalidate-contentpolicy for schema validation. APIM integrates with Azure DDoS Protection at the Premium tier. - On GCP, pair API Gateway with Cloud Armor for DDoS protection and request filtering. Apigee has built-in bot detection and threat protection.
Cost
- AWS HTTP API at 1 billion requests/month: ~$1,000. Lambda execution costs are separate.
- Azure APIM Standard: ~$750/month flat plus overage. Standard includes 1M calls/month; additional calls are $3.50/million.
- GCP API Gateway at 1 billion requests/month: ~$2,000 (calls over the free tier at $3.00/million above 2M).
Monitoring
- On AWS, enable CloudWatch detailed metrics for your API Gateway and set alarms on
4XXError,5XXError,Latency, andIntegrationLatency.IntegrationLatencyisolates backend latency from gateway latency — critical for cold start diagnosis. - On Azure, use APIM's built-in Application Insights integration. Track
Backend Durationseparately fromGateway Durationto identify whether slowness is in the gateway policies or the backend. - On GCP, Cloud Logging and Cloud Trace integrate automatically. Set a budget alert on API call costs in the GCP Console — GCP quotas can run up quickly on traffic spikes.
Full Example: API Aggregation Service (TypeScript)
This aggregation service sits behind an API Gateway (on any provider) and fans out to three downstream services in parallel. It returns a merged response or reports which upstream failed.
import express, { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';
const app = express();
app.use(express.json());
// ─── Types ───────────────────────────────────────────────────────────────────
/** Represents the shape of a downstream service response for an order. */
interface OrderData {
id: string;
status: string;
total: number;
lineItems: Array<{ productId: string; qty: number }>;
}
/** Inventory availability per product. */
interface InventoryData {
productId: string;
available: number;
reserved: number;
}
/** Shipment tracking details. */
interface ShipmentData {
trackingNumber: string;
carrier: string;
estimatedDelivery: string;
status: string;
}
/** Merged summary returned to the client. */
interface OrderSummaryResponse {
requestId: string;
order: OrderData;
inventory: InventoryData[];
shipment: ShipmentData | null;
}
/** RFC 7807 problem detail shape for error responses. */
interface ProblemDetail {
type: string;
title: string;
status: number;
detail: string;
instance?: string;
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
function problem(
res: Response,
status: number,
title: string,
detail: string,
instance?: string,
): void {
const body: ProblemDetail = {
type: `https://api.example.com/errors/${status}`,
title,
status,
detail,
...(instance ? { instance } : {}),
};
res.status(status).contentType('application/problem+json').json(body);
}
/**
* Fetch a downstream service and parse the JSON response.
*
* @remarks Throws with a structured error if the upstream returns non-2xx.
* Caller is responsible for catching via Promise.allSettled.
*
* @param url - Full URL of the downstream service endpoint.
* @returns Parsed JSON response body.
* @throws {Error} when upstream returns a non-2xx status.
*/
async function fetchUpstream<T>(url: string): Promise<T> {
const res = await fetch(url, {
headers: { 'X-Request-Id': randomUUID() },
signal: AbortSignal.timeout(5000), // 5 s timeout per upstream call
});
if (!res.ok) {
throw new Error(`Upstream ${url} returned ${res.status}`);
}
return res.json() as Promise<T>;
}
// ─── Routes ──────────────────────────────────────────────────────────────────
/**
* GET /orders/:id/summary — fan-out aggregation across order, inventory, and shipping services.
*
* @remarks
* **Path parameters:**
* - `id` — Order UUID. Must exist in the order service.
*
* **Responses:**
* - `200 OK` — All three upstreams responded successfully.
* - `404 Not Found` — Order service returned 404.
* - `502 Bad Gateway` — One or more upstreams failed or timed out; body identifies which.
*/
app.get('/orders/:id/summary', async (req: Request, res: Response) => {
const { id } = req.params;
const requestId = randomUUID();
const ORDER_SVC = process.env.ORDER_SERVICE_URL ?? 'http://order-service';
const INV_SVC = process.env.INVENTORY_SERVICE_URL ?? 'http://inventory-service';
const SHIP_SVC = process.env.SHIPPING_SERVICE_URL ?? 'http://shipping-service';
const [orderResult, shipmentResult] = await Promise.allSettled([
fetchUpstream<OrderData>(`${ORDER_SVC}/orders/${id}`),
fetchUpstream<ShipmentData>(`${SHIP_SVC}/shipments/${id}`),
]);
if (orderResult.status === 'rejected') {
problem(res, 502, 'Upstream Failure', `Order service failed: ${orderResult.reason}`, req.path);
return;
}
const order = orderResult.value;
// Fetch inventory for each line item in parallel
const inventoryResults = await Promise.allSettled(
order.lineItems.map(item =>
fetchUpstream<InventoryData>(`${INV_SVC}/stock/${item.productId}`),
),
);
const inventoryFailure = inventoryResults.find(r => r.status === 'rejected');
if (inventoryFailure) {
problem(res, 502, 'Upstream Failure', 'Inventory service failed for one or more products', req.path);
return;
}
const summary: OrderSummaryResponse = {
requestId,
order,
inventory: inventoryResults.map(r => (r as PromiseFulfilledResult<InventoryData>).value),
shipment: shipmentResult.status === 'fulfilled' ? shipmentResult.value : null,
};
res.json(summary);
});
// ─── Error handler ───────────────────────────────────────────────────────────
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
console.error('Unhandled error', { path: req.path, error: err.message });
problem(res, 500, 'Internal Server Error', 'An unexpected error occurred.', req.path);
});
// ─── Start ───────────────────────────────────────────────────────────────────
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
app.listen(PORT, () => {
console.log(`Aggregation service running on port ${PORT}`);
});
Run the service:
npm install
ORDER_SERVICE_URL=http://localhost:3001 \
INVENTORY_SERVICE_URL=http://localhost:3002 \
SHIPPING_SERVICE_URL=http://localhost:3003 \
npm run dev
Test the aggregation endpoint:
# Aggregated response from three services in one call
curl -s http://localhost:3000/orders/order-abc123/summary | jq
# Expected shape:
# {
# "requestId": "uuid",
# "order": { "id": "order-abc123", "status": "confirmed", ... },
# "inventory": [{ "productId": "...", "available": 10 }, ...],
# "shipment": { "trackingNumber": "...", "status": "in_transit" }
# }
Full source with tests: GitHub link →
cloud-apis/api-gateway-patterns/
Conclusion
AWS HTTP API is the right default for Lambda-backed services — it is faster and costs less than REST API, and covers 90% of use cases. Azure APIM wins when your organization needs a developer portal, enterprise API governance, or REST-to-SOAP translation without custom code. GCP's three options serve different tiers: API Gateway for serverless, Cloud Endpoints for gRPC, and Apigee for enterprise. The BFF pattern, API aggregation, and protocol translation are cloud-agnostic in concept but differ significantly in implementation cost — Azure APIM handles protocol translation natively, while AWS and GCP require adapter services. The decision you should walk away from this article having made: stop treating gateway selection as a checkbox in your cloud provider's onboarding wizard. The wrong choice costs you months of workarounds; the right one disappears into your infrastructure and you stop thinking about it entirely — which is exactly what a good gateway should do.
Further Reading
- AWS API Gateway Developer Guide — HTTP API vs REST API — official AWS documentation on choosing between the two products
- Azure API Management documentation — official Microsoft docs covering policies, tiers, and the developer portal
- Choosing between Apigee, API Gateway, and Cloud Endpoints — Google Cloud Blog — official Google guidance on selecting the right GCP product
- GCP API Gateway documentation — OpenAPI configuration, authentication, and quota management
If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.
Bry Writes Code — cloud and API infrastructure specialist. Evaluating or designing your API gateway architecture? Get in touch.






Top comments (0)