System Design

API Gateway Patterns: Routing, Auth, & Rate Limiting

Master API Gateway patterns: request routing, robust authentication, and effective rate limiting. Learn architecture, code examples, and best practices for scalable APIs.

Khader Vali September 2, 2026 17 min read

Mastering API Gateway Patterns: Request Routing, Authentication, and Rate Limiting

In today’s distributed systems landscape, microservices have become the de facto standard for building scalable, resilient, and independently deployable applications. While microservices offer immense benefits, they also introduce complexities, particularly when it comes to managing external access. This is where the API Gateway pattern shines, acting as the single entry point for all clients, simplifying client-side logic, and offloading cross-cutting concerns from individual services.

As a senior engineer, I’ve seen firsthand how a well-implemented API Gateway can transform a chaotic collection of services into a cohesive, secure, and performant system. In this comprehensive guide, we’ll dive deep into the API Gateway pattern, focusing on three critical functionalities: Request Routing, Authentication & Authorization, and Rate Limiting. We’ll explore their architectural implications, discuss various implementation strategies, provide practical code examples, and walk through real-world scenarios to solidify your understanding.

What is an API Gateway and Why Do We Need It?

At its core, an API Gateway is a server that acts as an API front-end, sitting between the client and a collection of backend services. It’s often referred to as a “traffic cop” or a “reverse proxy” on steroids. Instead of clients making direct requests to individual microservices (which could be numerous and constantly changing), they make a single request to the API Gateway. The Gateway then routes the request to the appropriate backend service, potentially aggregates results, and returns a unified response to the client.

The Challenges Microservices Introduce Without a Gateway:

  • Increased Complexity for Clients: Clients need to know the individual endpoints of potentially dozens of microservices, manage their varying protocols, and handle direct network calls.
  • Cross-Cutting Concerns Duplication: Each microservice might need to implement authentication, rate limiting, logging, caching, and other common functionalities. This leads to redundant code, maintenance overhead, and potential inconsistencies.
  • Security Vulnerabilities: Exposing all microservices directly to the internet increases the attack surface.
  • Service Discovery and Load Balancing: Clients would need to implement complex service discovery mechanisms and load balancing logic to find and distribute requests among multiple instances of a service.
  • Refactoring and Versioning Challenges: Changing a microservice’s API contract or deploying new versions can break clients if they’re directly coupled.

The API Gateway addresses these challenges by centralizing common functionalities and providing a consistent, unified API for clients. It acts as an abstraction layer, decoupling clients from the internal architecture of your microservices.

API Gateway Patterns: Routing, Auth, & Rate Limiting
Generated Image

The API Gateway Pattern Explained

The API Gateway pattern provides a single, unified entry point for a set of microservices. It’s not just a simple proxy; it’s a sophisticated component capable of handling a variety of responsibilities. Think of it as the control tower for all incoming and outgoing API traffic. It typically runs as a dedicated service or a set of services.

Key Responsibilities of an API Gateway:

  • Request Routing: Directing incoming requests to the correct backend service.
  • Authentication and Authorization: Verifying client identities and permissions before forwarding requests.
  • Rate Limiting: Controlling the number of requests a client can make within a specific timeframe to prevent abuse and ensure fair usage.
  • Response Aggregation: Combining responses from multiple services into a single, cohesive response for the client.
  • Protocol Translation: Converting requests from one protocol (e.g., HTTP/REST) to another (e.g., gRPC) for backend services.
  • Load Balancing: Distributing incoming requests across multiple instances of a service.
  • Caching: Storing frequently accessed data to reduce latency and backend load.
  • Logging and Monitoring: Centralizing request/response logging and providing metrics for operational insights.
  • Circuit Breaking: Preventing cascading failures in a distributed system by temporarily blocking requests to unhealthy services.
  • Security Policies: Enforcing security policies like WAF (Web Application Firewall) rules.

While an API Gateway can handle all these, it’s crucial to apply the “single responsibility principle” even here. Overloading your API Gateway can turn it into a monolithic bottleneck. The key is to choose which cross-cutting concerns are best handled at the edge (the gateway) versus within the services themselves.

Request Routing: Directing the Flow

Request routing is arguably the most fundamental function of an API Gateway. It’s the mechanism by which the gateway examines an incoming request and determines which backend service (or services) should handle it. This involves inspecting various parts of the request, such as the URL path, HTTP headers, query parameters, or even the HTTP method.

How Request Routing Works:

The API Gateway maintains a set of routing rules. When a request arrives, it matches the request against these rules. Each rule typically specifies:

  • Matching criteria: e.g., /users/*, Host: api.example.com, X-Version: v2.
  • Target service endpoint: The URL of the backend service to which the request should be forwarded.

Consider a scenario where you have a UserService at http://user-service:8081 and a ProductService at http://product-service:8082. Without an API Gateway, clients would need to know these distinct URLs. With a gateway, clients only interact with api.yourdomain.com.

Example Routing Flow (in words):

Client Request (api.yourdomain.com/users/123)
    |
    V
API Gateway
    | (Inspects path: /users/*)
    V
Match Routing Rule:
    IF Path STARTS WITH "/users"
    THEN Route to http://user-service:8081/users
    |
    V
Forward Request to UserService (http://user-service:8081/users/123)
    |
    V
UserService processes and responds
    |
    V
API Gateway receives response
    |
    V
API Gateway sends response back to Client

Types of Routing:

  • Path-based Routing: Most common. Routes requests based on the URL path.
    • /users/* routes to User Service.
    • /products/* routes to Product Service.
  • Host-based Routing: Routes based on the hostname. Useful for multi-tenancy or different API versions.
    • api.example.com routes to general API.
    • admin.example.com routes to admin-specific services.
  • Header-based Routing: Routes based on specific HTTP headers. Often used for A/B testing or canary deployments.
    • X-User-Group: premium routes to premium services.
    • X-Version: v2 routes to v2 of a service.
  • Query Parameter-based Routing: Routes based on query parameters. Less common for main routing, but can be used for specific cases.

Architectural Considerations for Routing:

  • Service Discovery: How does the API Gateway know the current network location (IP address and port) of your backend services, especially in a dynamic environment where services scale up and down? Solutions like Eureka, Consul, or Kubernetes service discovery are crucial here. The Gateway queries these registries to find healthy instances.
  • Load Balancing: Once a service is identified, the Gateway might need to distribute requests among multiple instances of that service. It often incorporates its own load balancing algorithms (e.g., round-robin, least connections, random).
  • Dynamic Routing: The ability to update routing rules without restarting the gateway is vital for continuous deployment and operational flexibility.
  • URL Rewriting: Sometimes the internal path of a service differs from the external path exposed by the gateway. The gateway can rewrite URLs before forwarding.

Code Example: Request Routing with Nginx

Nginx is a popular choice for an API Gateway due to its performance and flexibility. Here’s a basic Nginx configuration for path-based routing:

# /etc/nginx/nginx.conf or a site-specific conf file

http {
    upstream user_service {
        server user-service:8081; # Internal DNS name or IP for user service
        server user-service-replica:8081; # Another instance for load balancing
    }

    upstream product_service {
        server product-service:8082; # Internal DNS name or IP for product service
    }

    server {
        listen 80;
        server_name api.yourdomain.com;

        location /users/ {
            proxy_pass http://user_service/; # Trailing slash is important for path rewriting
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            # Add other headers as needed
        }

        location /products/ {
            proxy_pass http://product_service/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        # Catch-all for other paths or default responses
        location / {
            return 404 "Not Found";
        }
    }
}

In this Nginx configuration:

  • upstream blocks define groups of backend servers for load balancing.
  • location /users/ matches any request starting with /users/ and forwards it to the user_service upstream.
  • proxy_pass http://user_service/; routes the request. The trailing slash ensures that /users/123 is passed as /123 to the backend service. If you omit the slash, /users/123 would be passed as /users/123 to the backend.
  • proxy_set_header ensures important client headers (like Host and client IP) are forwarded to the backend.

Real-World Scenario: Gradual Rollouts with Routing

Imagine you’re deploying a new version of your `ProductService` (v2) but want to test it with a small percentage of users before a full rollout. You can configure your API Gateway to route requests to `v2` based on a header or a cookie for a select group, or even a percentage of all requests.

# Nginx example for canary deployment
location /products/ {
    # Check for a specific cookie or header for beta users
    if ($cookie_beta_user = "true") {
        proxy_pass http://product_service_v2/;
    }
    # Or for a percentage-based rollout (requires more advanced logic, often externalized)
    # This example is illustrative; real-world percentage routing is more complex.
    # if ($request_id ~* "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") {
    #     set $split 1000; # 1000 = 100%
    #     set $random_number $(perl -e 'print int(rand($split));');
    #     if ($random_number < 100) { # 10% of traffic
    #         proxy_pass http://product_service_v2/;
    #     }
    # }

    proxy_pass http://product_service_v1/; # Default to v1
    proxy_set_header Host $host;
}

This allows you to safely test new features with a subset of users, minimizing risk to your entire user base.

Authentication and Authorization: Securing Your APIs

Security is paramount for any public-facing API. The API Gateway is the ideal place to centralize authentication and authorization logic, ensuring that only legitimate and authorized clients can access your backend services. This offloads the burden from individual microservices, allowing them to focus purely on their business logic.

Authentication vs. Authorization:

  • Authentication: Verifying the identity of a client (who are you?). This usually involves checking credentials like API keys, username/password, or tokens (JWT, OAuth).
  • Authorization: Determining what an authenticated client is allowed to do (what can you access?). This involves checking permissions or roles associated with the client.

Why Centralize Security at the Gateway?

  • Single Point of Entry: All requests pass through the gateway, making it the perfect choke point for security checks.
  • Reduced Duplication: Avoids implementing authentication/authorization logic in every microservice.
  • Consistent Security Policy: Ensures all APIs adhere to the same security standards.
  • Simplified Microservices: Backend services receive already-authenticated and authorized requests, often with user context injected into headers.
  • Easier Credential Management: API keys, client secrets, and token validation logic can be managed centrally.

Common Authentication Mechanisms at the Gateway:

  1. API Keys: Simple, often passed in a header (e.g., X-API-Key) or query parameter. The gateway validates the key against a datastore. Suitable for basic access control but less secure than token-based approaches.
  2. JSON Web Tokens (JWT): A compact, URL-safe means of representing claims to be transferred between two parties. JWTs are signed (and optionally encrypted) and can carry user identity and roles. The gateway validates the JWT’s signature and expiration.
  3. OAuth 2.0: An authorization framework that enables an application to obtain limited access to a user’s resources on an HTTP service. The gateway typically validates the access token issued by an OAuth provider.

Architectural Flow for Authentication (JWT Example):

1. Client obtains JWT from Identity Provider (e.g., Auth0, Keycloak)
    |
    V
2. Client sends Request with JWT in Authorization header (Bearer token)
    |
    V
API Gateway
    |
    V (Authentication Module)
    |   - Extracts JWT from header
    |   - Verifies JWT signature (using public key from Identity Provider)
    |   - Checks JWT expiration
    |   - (Optional) Checks JWT claims (e.g., issuer, audience)
    |   - (Optional) Extracts user ID/roles from JWT payload
    |
    V (If valid)
    |   - Injects user ID/roles into request headers (e.g., X-User-ID, X-User-Roles)
    V
Request Routing to Backend Service
    |
    V
Backend Service
    |   - Receives request with trusted user context in headers
    |   - Performs fine-grained authorization (e.g., "Can this user access THIS specific resource?")
    V
Responds to API Gateway
    |
    V
API Gateway sends response back to Client

Code Example: JWT Authentication with an Express.js Gateway (Conceptual)

While dedicated gateways like Kong or AWS API Gateway are common, you can build a custom gateway with frameworks like Express.js for specific needs. Here’s a conceptual middleware:

// gateway/src/middleware/authMiddleware.js
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa'); // To fetch public keys from Identity Provider

// Configuration for your JWT provider (e.g., Auth0, Okta)
const authConfig = {
    domain: 'YOUR_AUTH_DOMAIN', // e.g., 'dev-1234.us.auth0.com'
    audience: 'YOUR_API_AUDIENCE', // e.g., 'https://api.yourdomain.com'
};

const client = jwksClient({
    jwksUri: `https://${authConfig.domain}/.well-known/jwks.json`,
});

function getKey(header, callback) {
    client.getSigningKey(header.kid, (err, key) => {
        if (err) {
            console.error('Error fetching signing key:', err);
            return callback(err);
        }
        const signingKey = key.publicKey || key.rsaPublicKey;
        callback(null, signingKey);
    });
}

const authenticateJWT = (req, res, next) => {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).send('Unauthorized: No token provided or malformed.');
    }

    const token = authHeader.split(' ')[1];

    jwt.verify(token, getKey, { audience: authConfig.audience, issuer: `https://${authConfig.domain}/` }, (err, user) => {
        if (err) {
            console.error('JWT Verification Error:', err);
            return res.status(403).send('Forbidden: Invalid token.');
        }

        // Token is valid, inject user info into request for downstream services
        req.user = user; // Contains claims from the JWT payload
        req.headers['X-User-ID'] = user.sub; // Standard for user ID
        req.headers['X-User-Roles'] = JSON.stringify(user.roles || []); // Example for roles

        next(); // Proceed to the next middleware or route handler
    });
};

module.exports = authenticateJWT;
// gateway/src/app.js (Express app)
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const authenticateJWT = require('./middleware/authMiddleware');

const app = express();
const PORT = process.env.PORT || 3000;

// Use the authentication middleware for protected routes
app.use('/api/*', authenticateJWT); // Apply to all routes under /api

// Define proxy targets for routing
const userServiceProxy = createProxyMiddleware({
    target: 'http://user-service:8081',
    changeOrigin: true,
    pathRewrite: { '^/api/users': '/users' }, // Rewrite /api/users to /users for backend
});

const productServiceProxy = createProxyMiddleware({
    target: 'http://product-service:8082',
    changeOrigin: true,
    pathRewrite: { '^/api/products': '/products' },
});

// Route requests to respective services
app.use('/api/users', userServiceProxy);
app.use('/api/products', productServiceProxy);

// Basic health check
app.get('/health', (req, res) => res.send('Gateway is healthy!'));

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

This example demonstrates how an Express.js gateway uses middleware to authenticate JWTs. If the token is valid, it injects user details into request headers, which are then forwarded to the backend services. The backend services can trust these headers and use them for fine-grained authorization.

Real-World Scenario: Role-Based Access Control (RBAC)

Imagine your API has endpoints for users, administrators, and guest users. After authenticating a user via JWT, the gateway can inspect the `roles` claim within the JWT payload. Based on these roles, it can then decide whether the user is authorized to access a particular route.

// Inside authMiddleware.js or a separate authorization middleware
const authorizeRoles = (allowedRoles) => (req, res, next) => {
    if (!req.user || !req.user.roles) {
        return res.status(403).send('Forbidden: No roles found for user.');
    }

    const userRoles = req.user.roles; // Assuming 'roles' is an array in the JWT payload

    const hasPermission = userRoles.some(role => allowedRoles.includes(role));

    if (hasPermission) {
        next();
    } else {
        res.status(403).send('Forbidden: Insufficient permissions.');
    }
};

// In app.js
// app.use('/api/*', authenticateJWT); // Authentication first
// app.use('/api/admin', authorizeRoles(['admin']), adminServiceProxy);
// app.use('/api/products', authorizeRoles(['admin', 'user', 'guest']), productServiceProxy);

This allows you to define access policies at the gateway level, preventing unauthorized requests from ever reaching your backend services.

Rate Limiting: Controlling API Consumption

Rate limiting is a crucial mechanism to control the rate at which a client can make requests to an API. It’s essential for maintaining API stability, preventing abuse (like DoS attacks or brute-force attempts), ensuring fair usage among clients, and managing infrastructure costs.

Why Rate Limit at the API Gateway?

  • Edge Protection: Blocks malicious or excessive traffic before it hits your backend services, protecting them from overload.
  • Centralized Policy: Enforces consistent rate limits across all or specific APIs.
  • Granular Control: Apply different limits based on client identity, API key, IP address, user role, or subscription tier.
  • Scalability: Dedicated rate limiting components in gateways are often highly optimized.

Key Concepts in Rate Limiting:

  • Limit: The maximum number of requests allowed (e.g., 100 requests).
  • Window: The time period over which the limit applies (e.g., per minute, per hour).
  • Identifier: How you identify a client (e.g., IP address, API key, authenticated user ID).
  • Burst: Allows a client to exceed the limit momentarily for a small burst of requests.

Common Rate Limiting Algorithms:

  1. Fixed Window Counter:
    • How it works: Divides time into fixed-size windows (e.g., 60 seconds). Each window has a counter. When a request comes in, increment the counter. If the counter exceeds the limit, block the request. At the end of the window, reset the counter.
    • Pros: Simple to implement.
    • Cons: “Burstiness” at window edges. A client could make `N` requests at the very end of one window and `N` requests at the very beginning of the next, effectively making `2N` requests in a short period around the boundary.
  2. Sliding Window Log:
    • How it works: Stores a timestamp for each request made by a client. When a new request arrives, remove all timestamps older than the current window. If the remaining count exceeds the limit, block the request.
    • Pros: Highly accurate, avoids the “burstiness” problem of fixed windows.
    • Cons: High memory consumption for storing all timestamps, computationally more expensive.
  3. Sliding Window Counter:
    • How it works: A hybrid approach. Uses two fixed windows: the current window and the previous window. When a request arrives, calculate a weighted average of the counts from the current and previous windows.
    • Pros: Less memory intensive than sliding window log, mitigates the edge problem better than fixed window.
    • Cons: Still an approximation, not perfectly accurate.
  4. Token Bucket:
    • How it works: Imagine a bucket with a fixed capacity. Tokens are added to the bucket at a constant rate. Each incoming request consumes one token. If the bucket is empty, the request is blocked.
    • Pros: Allows for bursts up to the bucket capacity, smooths out traffic.
    • Cons: Requires careful tuning of token generation rate and bucket capacity.
  5. Leaky Bucket:
    • How it works: Requests are added to a queue (the bucket). Requests are processed (leak out) from the queue at a constant rate. If the bucket is full, new requests are dropped.
    • Pros: Smooths out bursts into a steady stream, preventing backend overload.
    • Cons: Can introduce latency if the queue is long.

Implementation Strategies and Data Stores:

  • In-Memory: Simple for a single gateway instance, but not scalable in a distributed environment. Each gateway instance would have its own count, leading to inaccurate limits.
  • Distributed Cache (e.g., Redis): The most common and recommended approach for distributed gateways. Redis is fast and can store counters, timestamps, or tokens centrally. All gateway instances can read from and write to the same Redis instance, ensuring accurate global limits.
  • Database: Less performant than Redis for high-throughput scenarios due to I/O overhead, but suitable for lower-volume APIs or for persistent storage of quota information.

Code Example: Rate Limiting with Nginx

Nginx provides robust built-in rate limiting capabilities using the limit_req_zone and limit_req directives.

http {
    # Define a rate limiting zone
    # 'mylimit': name of the zone
    # '$binary_remote_addr': unique identifier (client IP address)
    # '10m': zone size (10 megabytes) - stores states for about 160,000 IPs
    # 'rate=10r/s': limit to 10 requests per second
    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

    # Another zone for authenticated users, perhaps less strict or based on user ID
    # Requires custom logic to map user ID to a variable
    # limit_req_zone $http_x_user_id zone=userlimit:10m rate=100r/s;

    server {
        listen 80;
        server_name api.yourdomain.com;

        location /users/ {
            # Apply the rate limit
            # 'zone=mylimit': use the defined zone
            # 'burst=20': allow a burst of 20 requests over the limit before delaying/rejecting
            # 'nodelay': if burst is exceeded, requests are rejected immediately, not delayed
            limit_req zone=mylimit burst=20 nodelay; # For unauthenticated users by IP

            # If you had user-specific limits
            # limit_req zone=userlimit burst=50 nodelay;

            proxy_pass http://user_service/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        location /products/search {
            # Stricter limit for an expensive operation
            limit_req zone=mylimit burst=5 nodelay rate=2r/s; # 2 requests per second
            proxy_pass http://product_service/;
            proxy_set_header Host $host;
        }
    }
}

In this Nginx config:

  • limit_req_zone defines a shared memory zone for storing request states. Here, it tracks requests per unique IP address at 10 requests per second.
Written by

Khader Vali

Senior Software Engineer specializing in cloud architecture, real-time systems, and enterprise-scale applications.

Share this article

Related Articles

API Gateway Patterns: Routing, Auth, Rate Limiting

Jul 12, 2026 · 14 min read

CQRS & Event Sourcing for Scalable Applications

Jun 29, 2026 · 17 min read

Building Real-time Collaborative Apps with Yjs & WebSockets

Jul 10, 2026 · 14 min read