js

How to Build a Production-Ready API Gateway with Express, Kong, and Redis

Learn to build a powerful API gateway using Express.js, Kong, and Redis to simplify microservices and boost reliability.

How to Build a Production-Ready API Gateway with Express, Kong, and Redis

I was building a microservices system last month. It started simply enough—a user service, a product service, a payment service. But then the problems began. My frontend app had to make calls to three different URLs. Adding authentication to each service was a mess. When the product service slowed down, it dragged the whole user experience down with it. I needed one front door for all my services, a smart traffic controller. That’s what led me to build a production-ready API gateway. Let’s build one together.

Think of an API gateway as the receptionist for your entire application. Instead of clients wandering through a maze of different service doors, they talk to one central point. This receptionist handles the boring but vital stuff: checking IDs, limiting how many requests someone can make, and deciding which internal team (or microservice) should handle the request. It makes your system easier to manage, more secure, and much more reliable.

Why combine Express.js, Kong, and Redis? Each plays a specific role. Express.js is our flexible, custom-built foundation. We write the logic here. Kong is a battle-tested, high-performance gateway that handles complex routing and load balancing out of the box. Redis is our lightning-fast memory store, perfect for tracking rate limits and managing shared state. Together, they give us both control and power.

Let’s start with the core. We’ll create a simple Express server that acts as a proxy. It receives a request, figures out which backend service it’s for, and forwards it. Here’s the basic structure.

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
const PORT = process.env.PORT || 3000;

// Define our backend services
const services = {
  users: 'http://localhost:4001',
  products: 'http://localhost:4002',
  orders: 'http://localhost:4003'
};

// Create proxy middleware for each service
Object.entries(services).forEach(([route, target]) => {
  app.use(`/api/${route}`, createProxyMiddleware({
    target: target,
    changeOrigin: true,
    pathRewrite: {
      [`^/api/${route}`]: '',
    },
  }));
});

app.listen(PORT, () => {
  console.log(`API Gateway running on port ${PORT}`);
});

This is a good start, but it’s dumb. It just passes requests along. What happens if the users service is down? The gateway will still send requests to it, causing errors for clients. We need to be smarter.

This is where the circuit breaker pattern comes in. It’s like an electrical circuit breaker for your software. If a service fails too many times, the “circuit” trips. For a short period, all new requests are immediately rejected with an error, without even trying the sick service. This gives the service time to recover and prevents your gateway from wasting resources and slowing down. After a while, the gateway lets one request through as a test. If it succeeds, the circuit closes and traffic resumes normally.

class CircuitBreaker {
  constructor(service, failureThreshold = 5, resetTimeout = 30000) {
    this.service = service;
    this.failureThreshold = failureThreshold;
    this.resetTimeout = resetTimeout;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failureCount = 0;
    this.nextAttempt = Date.now();
  }

  async callService(request) {
    if (this.state === 'OPEN') {
      if (Date.now() > this.nextAttempt) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Service unavailable. Circuit is OPEN.');
      }
    }

    try {
      const response = await this.service(request);
      this.success();
      return response;
    } catch (error) {
      this.failure();
      throw error;
    }
  }

  success() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }

  failure() {
    this.failureCount++;
    if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
      console.log(`Circuit breaker for ${this.service.name} is OPEN.`);
    }
  }
}

Now we have a basic proxy with a safety mechanism. But can you guess the next big problem? A single user or a bot could flood our services with thousands of requests, causing a denial-of-service. We need rate limiting.

Rate limiting controls how many requests a client can make in a given time window. The simplest method is a fixed window: “100 requests per hour.” But this has a flaw. If a user makes 100 requests at 1:59 PM, and another 100 at 2:01 PM, they’ve made 200 requests in two minutes, which might still overload the system. A better approach is the sliding window log. We use Redis to store a timestamp for every request from a user. To check a new request, we count how many timestamps they have within the last hour. This is more accurate.

const Redis = require('ioredis');
const redis = new Redis();

async function rateLimit(userId, limit = 100, windowMs = 3600000) {
  const key = `rate_limit:${userId}`;
  const now = Date.now();
  const windowStart = now - windowMs;

  // Add the new request timestamp
  await redis.zadd(key, now, now);

  // Remove all timestamps older than the window
  await redis.zremrangebyscore(key, 0, windowStart);

  // Count the requests within the window
  const requestCount = await redis.zcard(key);

  // Set an expiry on the key so Redis doesn't fill up
  await redis.expire(key, windowMs / 1000);

  if (requestCount > limit) {
    return { allowed: false, remaining: 0 };
  }
  return { allowed: true, remaining: limit - requestCount };
}

We’ve built smart logic in Express. But what about complex routing, load balancing across five instances of a service, or SSL termination? Writing all that from scratch is hard. This is where Kong shines. Kong is a gateway that runs separately. We configure it via a REST API or a config file. Our Express gateway can sit in front of Kong or use it for specific tasks.

For example, let’s use Kong to do round-robin load balancing between two instances of our users service. First, we define the upstream (the group of servers) and then a service that points to it.

# Add an upstream named 'users-upstream'
curl -i -X POST http://localhost:8001/upstreams \
  --data "name=users-upstream"

# Add two targets (servers) to that upstream
curl -i -X POST http://localhost:8001/upstreams/users-upstream/targets \
  --data "target=users1:4001" \
  --data "weight=100"

curl -i -X POST http://localhost:8001/upstreams/users-upstream/targets \
  --data "target=users2:4002" \
  --data "weight=100"

# Create a Kong service that routes to this upstream
curl -i -X POST http://localhost:8001/services \
  --data "name=users-service" \
  --data "host=users-upstream" \
  --data "path=/"

# Create a route so requests to /api/users go to this service
curl -i -X POST http://localhost:8001/services/users-service/routes \
  --data "paths[]=/api/users"

Now, Kong will distribute requests to users1:4001 and users2:4002 evenly. If one fails, Kong can stop sending traffic to it. We didn’t have to write any load-balancing code.

Let’s bring it all together. Our architecture has two layers. The first layer is our custom Express gateway. It handles application-level concerns: initial authentication, logging in our format, and maybe some request validation. It then forwards the request to Kong (running on localhost:8000). Kong handles the infrastructure-level concerns: finding the right service, load balancing, retrying failed requests, and terminating SSL. Finally, Kong sends the request to the actual microservice.

Why not just use Kong alone? Because Express gives us easy, fine-grained control. Why not just use Express? Because Kong gives us performance and features that are tedious to build. They complement each other.

What about monitoring? We need to know if our gateway is healthy. We can add a health check endpoint that checks the status of Redis and Kong.

app.get('/health', async (req, res) => {
  const health = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    checks: {
      redis: 'pending',
      kong: 'pending'
    }
  };

  // Check Redis
  try {
    await redis.ping();
    health.checks.redis = 'healthy';
  } catch (error) {
    health.checks.redis = 'unhealthy';
    health.status = 'unhealthy';
  }

  // Check Kong
  try {
    const response = await axios.get('http://kong:8001/status');
    if (response.data.server.connections_handled > 0) {
      health.checks.kong = 'healthy';
    } else {
      health.checks.kong = 'unhealthy';
      health.status = 'unhealthy';
    }
  } catch (error) {
    health.checks.kong = 'unhealthy';
    health.status = 'unhealthy';
  }

  res.status(health.status === 'healthy' ? 200 : 503).json(health);
});

Deploying this requires careful thought. We run each component in its own Docker container: one for the Express app, one for Kong, one for the Kong database, and one for Redis. We use a docker-compose.yml file to define how they all link together. In production, you’d use Kubernetes or a similar system to manage these containers, scale them up when traffic is high, and restart them if they crash.

Building this gateway transformed my project. My frontend code got simpler. My backend services became more focused. I could see exactly how my API was being used and stop problems before they affected users. It went from being a source of headaches to the stable core of the system.

This journey from a tangled mess of services to a coordinated system is what modern backend engineering is all about. It’s not just writing code; it’s designing a robust flow of information. I encourage you to take these concepts and adapt them. Start with the simple Express proxy, then add a circuit breaker. Connect it to Redis. Experiment with Kong’s admin API. Each piece you add makes your system more resilient.

Did you find this walkthrough helpful? Have you faced different challenges when building your own API gateway? I’d love to hear about your experiences. Please share your thoughts in the comments, and if this guide clarified things for you, consider sharing it with other developers who might be facing the same architectural puzzle.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: api gateway,express js,kong api gateway,redis,circuit breaker



Similar Posts
Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

Learn how to integrate Next.js with Prisma ORM for type-safe, full-stack applications. Build powerful database-driven web apps with ease. Start building today!

Blog Image
Build Multi-Tenant SaaS API with NestJS, Prisma, and Row-Level Security Tutorial

Learn to build secure multi-tenant SaaS APIs with NestJS, Prisma & PostgreSQL RLS. Master tenant isolation, authentication, and scalable architecture patterns.

Blog Image
Complete NestJS Production API Guide: PostgreSQL, Prisma, Authentication, Testing & Docker Deployment

Learn to build production-ready REST APIs with NestJS, Prisma & PostgreSQL. Complete guide covering authentication, testing, Docker deployment & more.

Blog Image
Complete Guide to Building Full-Stack TypeScript Apps with Next.js and Prisma Integration

Learn how to integrate Next.js with Prisma for type-safe full-stack TypeScript apps. Build modern web applications with seamless database operations.

Blog Image
Complete Guide to Building Modern Web Apps with Svelte and Supabase Integration

Learn to integrate Svelte with Supabase for high-performance web apps. Build real-time applications with authentication, database, and storage. Start today!

Blog Image
Production-Ready Rate Limiting System: Redis and Express.js Implementation Guide with Advanced Algorithms

Learn to build a robust rate limiting system using Redis and Express.js. Master multiple algorithms, handle production edge cases, and implement monitoring for scalable API protection.