Quick Answer

An API gateway is a single service between clients and your backend services. Every request goes through it, and it handles the concerns that would otherwise be duplicated in every service: routing a request to the right backend, checking authentication, enforcing rate limits, terminating TLS, and sometimes combining several backend calls into one response. The trade-off is one extra network hop, one more thing to deploy, and a single point of failure unless you run it redundantly.

The problem: clients talking to many services

In a microservices setup a single screen might need four services - users, orders, inventory, recommendations - on four different hosts. Without a gateway, the browser or mobile app has to know all four URLs, implement authentication four times, deal with CORS for four origins, make four round trips over the public internet, and break whenever a service is split, moved, or renamed.

Every client type - web, iOS, Android, a partner integration - repeats that same work independently, so a change to how auth tokens are passed means shipping four apps. And cross-cutting concerns like rate limiting and request logging get copy-pasted into all four services, where they slowly drift out of sync until each one enforces slightly different rules.

A gateway collapses this to one URL, one authentication scheme, and one place where the shared logic lives. Clients stop caring how many services sit behind it, what language those services are written in, or where they run. Backend teams can split a service in two without a single client noticing, as long as the gateway keeps routing the same paths.

What a gateway actually does

The core jobs: route each request to the right upstream by path or host; validate the auth token once and reject bad requests before they reach any backend; enforce rate limits; terminate TLS so internal traffic can be plain HTTP on a private network; and give you one place to log every request and attach a trace ID.

const routes = [
  { prefix: '/api/users',  target: 'http://localhost:5001' },
  { prefix: '/api/orders', target: 'http://localhost:5002' },
];

const gateway = http.createServer(async (req, res) => {
  // one auth check for everything behind the gateway
  if (req.headers['authorization'] !== 'Bearer demo-token') {
    res.statusCode = 401;
    return res.end(JSON.stringify({ error: 'missing or bad token' }));
  }
  const route = routes.find((r) => req.url.startsWith(r.prefix));
  if (!route) { res.statusCode = 404; return res.end('{"error":"no route"}'); }

  const upstream = await getJSON(route.target + req.url.replace('/api', ''));
  res.statusCode = upstream.status;
  res.end(JSON.stringify(upstream.body));
});
no token       -> 401 {"error":"missing or bad token"}
routed /users  -> 200 {"id":7,"name":"Asha"}
routed /orders -> 200 [{"id":"A1","total":900},{"id":"A2","total":250}]

This is a teaching version. In production you configure an existing gateway - nginx, Envoy, Kong, Traefik, or a managed one like AWS API Gateway - rather than hand-writing one.

Aggregation and the backend-for-frontend

A gateway can also combine calls. The client hits one endpoint, the gateway fans out to several services, merges the results, and returns a single response - saving a mobile client several slow round trips:

if (req.url === '/api/dashboard?user=7') {
  const [user, orders] = await Promise.all([
    getJSON('http://localhost:5001/users/7'),
    getJSON('http://localhost:5002/orders?user=7'),
  ]);
  return res.end(JSON.stringify({
    name: user.body.name,
    orderCount: orders.body.length,
    lifetimeValue: orders.body.reduce((sum, o) => sum + o.total, 0),
  }));
}
aggregated -> 200 {"name":"Asha","orderCount":2,"lifetimeValue":1150}

When different client types need different response shapes, this becomes the Backend for Frontend pattern: a dedicated gateway per client, so the mobile app gets a lean payload and the web app a richer one without a single gateway trying to serve both. Keep aggregation to fetching and reshaping only - no business rules, no writes with side effects. A gateway that grows business logic becomes the worst part of a distributed monolith.

The gotcha: one slow backend drags down the whole response

Naive aggregation with Promise.all fails entirely if any one upstream fails. Your dashboard shows nothing because the recommendations service - the least important part of the page - happens to be down.

// users service is healthy; orders service is DOWN
async function naiveDashboard() {                 // Promise.all
  const [user, orders] = await Promise.all([userCall(), ordersCall()]);
  return { name: user.name, orderCount: orders.length };
}
async function resilientDashboard() {             // Promise.allSettled + timeout
  const [user, orders] = await Promise.allSettled([userCall(), ordersCall()]);
  return {
    name: user.status === 'fulfilled' ? user.value.name : null,
    orders: orders.status === 'fulfilled' ? orders.value : 'unavailable',
  };
}
naive     -> FAILED: ECONNREFUSED (whole dashboard is now a 500)
resilient -> { name: 'Asha', orders: 'unavailable' }

Rules for gateway aggregation: give every upstream call a short timeout, treat each as independently failable, and return partial data with a clear marker instead of a 500. This is also exactly where a per-upstream circuit breaker belongs.

What a gateway costs you

Single point of failure. If the gateway is down, everything is down. Run several instances behind a load balancer and keep it stateless.

Latency. Every request now takes one more network hop and possibly one more TLS handshake. Usually single-digit milliseconds, but it is not zero, and a gateway doing heavy transforms can become the bottleneck.

An organisational chokepoint. If every team needs the gateway team to change a config before they can ship, the gateway becomes everyone's blocker. Favour declarative, version-controlled route config that teams can change by pull request.

Also: developers now need the gateway running locally too, or a documented way to bypass it. For a single backend service you do not need a gateway at all - a reverse proxy like nginx for TLS and rate limiting is enough. The pattern earns its keep once you have several services and more than one kind of client.

Frequently Asked Questions

What's the difference between an API gateway and a load balancer? A load balancer spreads traffic across identical copies of one service. A gateway routes to different services by path or host and adds application concerns like authentication, rate limiting, and response aggregation.
Is an API gateway just a reverse proxy? It is a reverse proxy with more responsibilities. Every gateway proxies requests, but it also handles auth, rate limiting, aggregation, and API-specific routing that a plain reverse proxy does not.
Does an API gateway slow down my API? It adds one network hop, usually a few milliseconds. That is often outweighed by fewer client round trips and offloaded TLS and auth work, but a gateway doing heavy aggregation or transforms can become a bottleneck.
Should a small project use an API gateway? If you have one backend service, no - use a reverse proxy for TLS and rate limiting. The pattern pays off once you have multiple services and multiple client types that would otherwise duplicate routing and auth.
Should authentication happen at the gateway or in the services? Validate the token at the gateway so bad requests are rejected early, then pass verified identity to services as a trusted header. Services still enforce their own rules about what that identity may do.