Published on Monday, 19 May 2025
Tags: microservices1 api-gateway1 architecture1 backend1
Why Your Microservices Need an API Gateway (And Why You Probably Got It Wrong)
A practical guide to API gateways in microservice architectures — what they solve, what they break, and how to avoid the most common implementation mistakes senior devs make.
Table of Content
I've lost count of how many times I've seen a team slap an API gateway in front of their microservices and call it a day. The gateway becomes a dumping ground for every cross-cutting concern: auth, rate limiting, logging, request transformation, response caching, protocol translation, and — my personal favorite — business logic that should have stayed in the service.
Let's be clear: an API gateway is not a silver bullet. It's a sharp tool that solves a specific set of problems, but it introduces its own. I learned this the hard way after untangling a monolithic gateway that had become the single most critical failure point in a production system. Here's what I wish someone had told me before we started.
What an API Gateway Actually Does
An API gateway sits between your clients and your microservices. It's the single entry point for all external requests. The core responsibilities are:
- Routing: Forward requests to the correct internal service based on path, headers, or query parameters.
- Authentication and Authorization: Validate tokens, enforce policies before the request reaches your service.
- Rate Limiting: Protect downstream services from traffic spikes.
- Request/Response Transformation: Adapt payloads between client expectations and internal contracts.
- Aggregation: Combine results from multiple services into a single response.
- Protocol Translation: Convert between HTTP, WebSocket, gRPC, or message queues.
Here's what it should not do: implement business logic, store state, or become a proxy for database queries.
The Two Most Common Mistakes
Mistake #1: The Leaky Gateway
I once worked on a system where the gateway was doing field-level validation, enriching payloads with data from three different databases, and even running a cron job to clean up stale sessions. The gateway had become a microservice itself — an untestable, unscaleable monolith that was harder to deploy than any of the actual services.
The rule of thumb: if your gateway needs a database migration, you've already lost.
// ❌ Bad: Gateway doing business logic and database calls
app.post('/orders', async (req, res) => {
const user = await db.users.findById(req.body.userId);
if (!user.active) return res.status(403).send('Inactive user');
const inventory = await db.products.checkStock(req.body.productId);
if (inventory < req.body.quantity) return res.status(400).send('Out of stock');
// Gateway should not be calculating discounts
const discount = user.tier === 'premium' ? 0.1 : 0;
const total = req.body.quantity * req.body.price * (1 - discount);
// Forward to order service
const order = await axios.post('http://order-service/orders', {
...req.body,
total,
appliedDiscount: discount
});
res.json(order.data);
});The fix is simple: the gateway should validate that the request is structurally sound (required fields, correct types), then forward it. Let the order service handle business rules, stock checks, and pricing.
// ✅ Good: Gateway only validates structure and forwards
app.post('/orders', async (req, res) => {
const { error } = validateOrderSchema(req.body);
if (error) return res.status(400).json({ error: error.details });
// Forward with auth context attached
const response = await axios.post('http://order-service/orders', req.body, {
headers: { 'x-user-id': req.user.id, 'x-user-tier': req.user.tier }
});
res.status(response.status).json(response.data);
});Mistake #2: The Single Point of Pain
If your gateway goes down, everything goes down. I've seen teams put all their traffic through a single gateway instance with no redundancy, then wonder why a deployment window caused a 45-minute outage.
The solution is not to avoid a gateway — it's to design for failure. Use a load balancer in front of multiple gateway instances. Implement circuit breakers so that if a downstream service is slow, the gateway fails fast instead of hanging. And for god's sake, don't put state in the gateway.
version: '3.8'
services:
gateway:
image: myapp/gateway:latest
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 256M
environment:
- REDIS_URL=redis://redis:6379
- RATE_LIMIT=100
depends_on:
- redis
ports:
- "80:8080"Notice the environment variables for configuration. Your gateway should be stateless and configurable via environment or a centralized config service, not hardcoded.
When You Shouldn't Use an API Gateway
Not every architecture needs one. If you have:
- Fewer than 3 microservices
- All services speaking the same protocol (usually HTTP/JSON)
- Simple routing that can be handled by a reverse proxy (nginx, Caddy)
- No need for cross-cutting concerns like rate limiting or auth at the edge
...then you might be overengineering it. Start with a reverse proxy and add a gateway when the pain becomes real. Premature gateway adoption is a form of accidental complexity.
The One Pattern That Saved My Sanity
After years of trial and error, the pattern that consistently works is thin gateway, thick services. The gateway handles edge concerns (auth, TLS termination, rate limiting, basic validation). The services handle everything else.
Here's the concrete implementation I use now:
const express = require('express');
const rateLimit = require('express-rate-limit');
const { authenticate } = require('./middleware/auth');
const { validateSchema } = require('./middleware/validation');
const { proxy } = require('./lib/proxy');
const app = express();
// Edge concerns only
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use(authenticate);
app.use(express.json());
// Each route is a thin proxy
app.post('/api/orders', validateSchema(orderSchema), proxy('http://order-service'));
app.get('/api/products/:id', proxy('http://catalog-service'));
app.post('/api/payments', validateSchema(paymentSchema), proxy('http://payment-service'));
// Health check — don't proxy this
app.get('/health', (req, res) => res.json({ status: 'ok' }));
app.listen(8080);The proxy function is a generic handler that forwards the request and returns the response. No business logic, no database calls, no state.
The Bottom Line
An API gateway is a tool for managing edge complexity, not a place to dump your cross-cutting concerns. Keep it thin. Keep it stateless. And if you ever find yourself writing business logic in a gateway middleware, step away from the keyboard and think about what you're doing.
Your future self — and the poor developer who has to debug a production incident at 3 AM — will thank you.