Skip to main content

🚪 API Gateways

Once your app is a dozen services, a hard question appears: how does a browser or mobile app talk to it? Should the client know every service's address, auth scheme, and protocol? The answer, almost always, is no. An API gateway is the single front door that hides all of that — one entry point that routes, secures, and shapes traffic on its way to your services. This lesson shows you what it does, how to build one, and how to keep it from becoming the very bottleneck it was meant to prevent.

Week 14 · Wednesday: Microservices Architecture · Lecture 3

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain why a single entry point in front of many services beats letting clients call each service directly
  • Enumerate a gateway's core responsibilities: routing, auth/authz, rate limiting, TLS termination, aggregation, and caching
  • Implement request aggregation so one client call fans out to several services and returns a combined response
  • Apply the Backend-for-Frontend (BFF) pattern to give web, mobile, and third-party clients tailored gateways
  • Compare gateway options — Kong, NGINX, AWS API Gateway, Express Gateway, and a custom Express build
  • Avoid the gateway anti-patterns: the single point of failure, the performance bottleneck, and the god object full of business logic

Estimated Time: 75 minutes

Practice: Design and sketch a gateway (with a mobile BFF) for a five-service e-commerce app, then decide what belongs in it and what does not.

In This Lesson

The Single Front Door

An API gateway is a server that sits between your clients and your microservices, acting as the single entry point for every incoming request. Instead of a client talking to the User service, then the Product service, then the Order service — each with its own host, port, auth, and quirks — the client talks only to the gateway, which routes each request to the right place behind the scenes.

Think of a large hotel's concierge. Guests don't need to know that housekeeping is on the third floor, the kitchen handles room service, and a separate desk books tours. They tell the concierge what they want, and the concierge routes each request to the right department. The API gateway is that concierge for your services.

Without a gateway, each client calls every service directly; with a gateway, all clients call one entry point that routes to the services Without a gateway Client Users Products Orders client knows every service With a gateway Client API Gateway Users Products Orders client knows one address
The gateway collapses N client-to-service relationships into one. Clients get a single stable address; services stay private and free to change.

What does that single front door buy you?

  • Simpler clients. One base URL, one auth scheme, one place to point at — instead of juggling many endpoints.
  • Services stay private. Internal services aren't exposed to the internet; they can move, split, and rename freely behind the gateway.
  • Cross-cutting concerns in one place. Auth, rate limiting, TLS, logging — implemented once at the edge instead of copy-pasted into every service.
  • Fewer round trips. The gateway can aggregate several service calls into one response, which matters most on slow mobile networks.

What a Gateway Does

A gateway earns its place by taking on the cross-cutting concerns that every service would otherwise implement itself. Here are the core responsibilities.

graph LR A["Incoming request"] --> B["TLS termination"] B --> C["Rate limiting"] C --> D["Authenticate and authorize"] D --> E["Route to a service"] E --> F["Backend service"]

Routing

The gateway's most basic job: match the incoming path and forward it to the right service, often rewriting the path on the way (a public /api/users maps to the service's internal /api/v1/users).

// A minimal routing gateway with Express + http-proxy-middleware.
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();

// Forward /api/users/* to the User service, rewriting the prefix.
app.use('/api/users', createProxyMiddleware({
    target: 'http://user-service',
    changeOrigin: true,
    pathRewrite: { '^/api/users': '/api/v1/users' }
}));

app.use('/api/products', createProxyMiddleware({
    target: 'http://product-service',
    changeOrigin: true
}));

app.listen(3000, () => console.log('Gateway listening on :3000'));

Authentication & authorization

Verify who the caller is (authentication) and what they're allowed to do (authorization) once, at the edge, before the request ever reaches a service. Services then trust the gateway's verdict — commonly passed on as a validated token or trusted headers. This is a huge win: your services stop each re-implementing token parsing.

// Validate a JWT at the gateway; reject before proxying.
const jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
    const header = req.headers.authorization; // "Bearer <token>"
    if (!header) return res.status(401).json({ error: 'Authentication required' });

    try {
        const token = header.split(' ')[1];
        req.user = jwt.verify(token, process.env.JWT_SECRET); // throws if invalid
        next();
    } catch {
        return res.status(403).json({ error: 'Invalid or expired token' });
    }
}

// Protected routes run authenticate first, then proxy.
app.use('/api/orders', authenticate, createProxyMiddleware({
    target: 'http://order-service',
    changeOrigin: true
}));

Rate limiting

Protect services from abuse and traffic spikes by capping how many requests a client can make in a window. Enforced at the edge, it stops a misbehaving client before it ever touches your services.

const rateLimit = require('express-rate-limit');

// 100 requests per minute per IP for general traffic.
const generalLimiter = rateLimit({
    windowMs: 60 * 1000,
    max: 100,
    standardHeaders: true,      // send RateLimit-* headers
    legacyHeaders: false,
    message: 'Too many requests, please slow down.'
});

// A stricter cap for sensitive endpoints like login.
const loginLimiter = rateLimit({ windowMs: 60 * 1000, max: 10 });

app.use(generalLimiter);
app.use('/api/auth/login', loginLimiter);

TLS termination

The gateway is where HTTPS ends. It decrypts incoming TLS once, then forwards plain (or re-encrypted) traffic over the trusted internal network. Certificates live in one place instead of on every service, and internal services don't each pay the TLS handshake cost.

Caching

For responses that don't change per user and don't change often — a product catalog, reference data — the gateway can cache the response and serve it directly on the next request. The service isn't even touched, cutting both latency and load.

💡 Cross-cutting means "cuts across every service"

The unifying theme: these are concerns every service shares but none of them is really about. Auth, throttling, TLS, and logging aren't the Product service's job — they're plumbing. Pulling them up to the gateway lets each service focus on its actual business capability, and lets you change the plumbing in one place.

Request Aggregation

One of the gateway's most valuable tricks is aggregation (also called API composition): the client makes a single request, and the gateway fans it out to several services, then stitches the results into one response. This is a lifesaver on mobile, where each extra round trip over a flaky cellular network costs real time.

Consider a product-detail screen that needs the product, its reviews, and its live inventory — three services. Without a gateway, the app makes three calls and assembles the data itself. With aggregation, it makes one.

sequenceDiagram participant C as Mobile Client participant G as API Gateway participant P as Product Service participant R as Review Service participant I as Inventory Service C->>G: GET the product detail page for one product G->>P: Fetch the product P->>G: Product data G->>R: Fetch the reviews R->>G: Review list G->>I: Fetch the stock level I->>G: Inventory data G->>C: One combined response
// Aggregation endpoint: one client call, three services, one response.
app.get('/api/product-page/:id', async (req, res) => {
    const { id } = req.params;
    try {
        // Fire the independent calls in parallel — don't serialize them.
        const [product, reviews, inventory] = await Promise.all([
            fetch(`http://product-service/api/products/${id}`).then(r => r.json()),
            fetch(`http://review-service/api/reviews?productId=${id}`).then(r => r.json()),
            fetch(`http://inventory-service/api/stock/${id}`).then(r => r.json())
        ]);

        // Compose one payload shaped for the screen that needs it.
        res.json({ ...product, reviews, inStock: inventory.available });
    } catch (err) {
        console.error('Aggregation failed:', err);
        res.status(502).json({ error: 'Could not assemble the product page' });
    }
});

✅ Aggregate in parallel, and degrade gracefully

Two details make aggregation good instead of dangerous. First, run independent calls with Promise.all so the total time is the slowest single call, not the sum of all of them. Second, decide what happens when one call fails: if reviews are down, you probably still want to show the product with an empty reviews list rather than failing the whole page. Aggregation concentrates several dependencies into one endpoint, so its failure handling matters.

Backend for Frontend

A single gateway serving every client type eventually strains: a mobile app wants small, tightly-shaped payloads to save bandwidth and battery; a web dashboard wants rich, data-heavy responses; a third-party partner wants a stable public contract. Bending one gateway to satisfy all three makes it a mess.

The Backend-for-Frontend (BFF) pattern answers this: build a separate gateway for each client experience, each owned by the team that builds that client, each tailored to exactly what its client needs.

graph TD A["Web App"] --> B["Web BFF"] C["Mobile App"] --> D["Mobile BFF"] E["Partner Client"] --> F["Public API BFF"] B --> G["User Service"] B --> H["Product Service"] B --> I["Order Service"] D --> G D --> H D --> I F --> H

Picture specialized concierges: one trained for business travelers who knows meeting rooms and expense receipts, one for families who knows kid-friendly activities, one for tour groups who coordinates large movements. Same hotel, same underlying departments — but each guest gets an interface shaped for them. Netflix famously does this, with device-specific BFFs so a TV app and a phone app each get responses optimized for their screen and interaction model.

The trade-off: BFFs mean more gateways to build, deploy, and keep consistent, and some logic (like auth) gets repeated across them. Reach for BFFs when client needs genuinely diverge; a single gateway is the right default until they do.

Gateway Products

You rarely build a production gateway from scratch. Mature products handle routing, auth, rate limiting, and observability out of the box. The main options:

ProductWhat it isBest when
KongOpen-source gateway built on NGINX, extended with a rich plugin ecosystem (auth, rate limiting, logging); configured declarativelyYou want a powerful, self-hosted, plugin-driven gateway across any cloud
NGINXThe battle-tested reverse proxy and load balancer many gateways are built on; can act as a lean gateway itselfYou need raw performance and are comfortable configuring routing and TLS yourself
AWS API GatewayFully managed gateway that integrates with Lambda, Cognito, and IAM; you pay per requestYou're on AWS and want zero servers to operate, especially with serverless backends
Express GatewayA gateway built on Express and Node.js, configured with YAMLYour team lives in Node and wants a familiar, hackable gateway

Most of these are configured declaratively — you describe the desired routing and policies in a file, check it into version control, and the gateway applies it. Here's a taste of Kong's declarative config:

# kong.yml — routing and per-service policies, all declared.
_format_version: "3.0"

services:
  - name: user-service
    url: http://user-service:3000
    routes:
      - name: user-routes
        paths: ["/api/users", "/api/auth"]
    plugins:
      - name: rate-limiting
        config: { minute: 60, policy: local }
      - name: jwt          # validate JWTs at the edge

  - name: product-service
    url: http://product-service:3000
    routes:
      - name: product-routes
        paths: ["/api/products"]
    plugins:
      - name: rate-limiting
        config: { minute: 200, policy: local }
      - name: cors         # products are public: allow browsers

💡 A custom Express gateway is fine for learning — and small systems

The Express snippets in this lesson are a real, working gateway, and for a handful of services they're perfectly adequate. But as you add TLS, distributed rate limiting, metrics, connection pooling, and high-availability, you're slowly rebuilding Kong. Know how a gateway works by building a small one; reach for a product before you reinvent all of it.

Pitfalls: Bottleneck & God Object

The gateway's greatest strength — everything passes through it — is also its greatest danger. Three failure modes are worth naming.

Single point of failure

If every request flows through the gateway and the gateway goes down, your entire API goes down — not one feature, everything. The mitigation is non-negotiable: never run a single instance. Deploy several behind a load balancer, spread across availability zones, so any one can fail without taking the system with it. A gateway must be designed for high availability precisely because so much depends on it.

Performance bottleneck

Every request pays the cost of the extra hop through the gateway, and every request competes for the gateway's CPU and connections. If the gateway is under-provisioned or does heavy work per request, it becomes the slowest part of every call. Keep it lean: fast, non-blocking I/O; connection pooling to backends; caching where it helps; and horizontal scaling so you can add instances as traffic grows.

The god object

This is the subtle, insidious one. Because the gateway is a convenient central place, teams keep adding "just one more thing" — a bit of business logic here, a data transformation there, an orchestration workflow, some domain rules. Over time the gateway swells into a god object: a bloated component that knows about every service's internals, that every team must coordinate to change, and that quietly becomes a new monolith at the center of your architecture.

⚠️ Keep business logic OUT of the gateway

The rule that prevents the god object: the gateway handles generic, cross-cutting concerns — routing, auth, rate limiting, TLS, aggregation of existing responses. It must not own business logic — pricing rules, order validation, domain decisions. Those belong in the services that own that capability. When you feel tempted to put a business rule in the gateway "because it's easier," that's the warning sign. A gateway that knows why an order is valid has stopped being infrastructure and started being a monolith.

graph LR A["Client"] --> B["Load Balancer"] B --> C["Gateway Instance 1"] B --> D["Gateway Instance 2"] B --> E["Gateway Instance 3"] C --> F["Services"] D --> F E --> F

The healthy shape: multiple lean, stateless gateway instances behind a load balancer, each doing generic edge work and nothing more. Stateless is what makes them freely scalable — no instance holds data another needs.

Practice & Quiz

🏋️ Exercise 1: Sort the responsibilities

Goal: For an e-commerce app with User, Product, Order, and Payment services, decide for each item whether it belongs in the gateway or in a service: (a) validating the caller's JWT, (b) calculating an order's total with tax and discounts, (c) limiting each IP to 100 requests/minute, (d) deciding whether a coupon code is still valid, (e) combining product + reviews + inventory into one product-page response.

💡 Hint

Ask: is this a generic edge concern every service shares, or a business decision that requires knowing a specific domain's rules? Edge concerns go in the gateway; domain rules go in the owning service.

✅ Solution
  • (a) Validate JWT — gateway. Authentication is a cross-cutting edge concern; do it once for everyone.
  • (b) Calculate order total — service. Pricing, tax, and discounts are business logic that belongs to the Order (or a Pricing) service. Putting it in the gateway is the god-object trap.
  • (c) Rate limit per IP — gateway. Protecting the system from abuse is classic edge work.
  • (d) Validate a coupon — service. Coupon rules are domain logic owned by the service that manages promotions; the gateway must not know them.
  • (e) Combine into a product page — gateway. Aggregating existing service responses into one payload is a legitimate gateway job — as long as it only composes responses and doesn't compute business rules over them.

The dividing line throughout: the gateway moves and shapes traffic; services decide things.

🏋️ Exercise 2: Design a mobile BFF

Goal: Your web app already uses a single gateway. The new mobile team complains that responses are huge and every screen needs three or four calls. In two or three sentences, describe how a BFF helps and one cost it introduces.

✅ Solution

Give the mobile team their own Mobile BFF that they own. It can trim payloads to just the fields the mobile screens use (saving bandwidth and battery) and aggregate the three-or-four calls each screen needs into a single request, cutting round trips over slow cellular networks. The cost is another gateway to build, deploy, and monitor, plus some duplicated concerns (like auth setup) across the web and mobile BFFs — worth it once the two clients' needs have genuinely diverged, but not before.

🎯 Quick Quiz

Question 1: Which task does not belong in an API gateway?

Question 2: A mobile screen needs data from three services and you want a single client request. Which gateway capability provides this?

Question 3: What is the primary defense against the gateway being a single point of failure?

Best Practices & Pitfalls

✅ Do

  • Put generic cross-cutting concerns — routing, auth, rate limiting, TLS, logging — in the gateway, once
  • Run multiple stateless gateway instances behind a load balancer for high availability
  • Keep the gateway lean: non-blocking I/O, connection pooling, caching where it helps
  • Aggregate with parallel calls (Promise.all) and decide how to degrade when one dependency fails
  • Reach for a proven product (Kong, NGINX, AWS API Gateway, Express Gateway) before rebuilding one
  • Use BFFs when client needs genuinely diverge, and let client teams own them
  • Configure routing and policies declaratively and keep that config in version control

❌ Don't

  • Put business logic — pricing, validation, domain rules — in the gateway; that's the god object
  • Run a single gateway instance; it becomes a system-wide single point of failure
  • Let the gateway hold per-request state that would block horizontal scaling
  • Do heavy synchronous work per request that turns the gateway into the slowest hop
  • Spin up a BFF per client before the clients' needs have actually diverged
  • Expose internal services directly and bypass the gateway's security

⚠️ The gateway is infrastructure, not an application

Hold onto one mental model and most gateway mistakes disappear: the gateway is a smart doorway, not a brain. It moves, secures, and shapes traffic. The moment it starts deciding business outcomes, it has drifted from infrastructure into application logic — and a central application that every team must coordinate to change is exactly the monolith microservices were meant to escape.

Summary

🎉 Key Takeaways

  • An API gateway is the single entry point in front of many services — simpler clients, private services, one place for edge concerns
  • Core responsibilities: routing, authentication/authorization, rate limiting, TLS termination, request aggregation, and caching
  • Aggregation collapses several service calls into one client response — run them in parallel and degrade gracefully
  • The BFF pattern gives web, mobile, and partner clients tailored gateways when their needs diverge
  • Prefer proven products — Kong, NGINX, AWS API Gateway, Express Gateway — over a from-scratch build at scale
  • Avoid the anti-patterns: the single point of failure, the performance bottleneck, and the god object stuffed with business logic

📚 Additional Resources

🚀 What's Next?

You've now seen asynchronous messaging mentioned as the antidote to synchronous coupling — both in service communication and in gateway aggregation. Next up: Message Queue Concepts — a deep look at how brokers actually work, the queue and pub/sub models, delivery guarantees, acknowledgements, and dead-letter queues, so you can build the event-driven backbone that keeps services loosely coupled.

🎉 Great work!

You can design a front door that makes a dozen services look like one clean API — and you know exactly what to keep out of it.