# Microservices: The API Gateway

* * *

***Prerequisite:*** *Before diving into this article, I would highly recommend to go through* [`Microservices: The Ultimate Practical`](https://google.com) *article.*

* * *

In this article, we will implement the following flow:

1.  **Client request** – The client application sends a request to the API Gateway to fetch all users.
    
2.  **API Gateway** – The API Gateway intelligently routes the request to the User microservice.
    
3.  **User Service** – The User Service receives the request and begins processing.
    
4.  **Rating Service** – The User Service communicates with the Rating Service to retrieve all ratings associated with each user.
    
5.  **Hotel Service** – Simultaneously, the User Service contacts the Hotel Service to fetch details about the hotels linked to those ratings.
    
6.  **Response aggregation** – The User Service combines the data from both the Rating Service and the Hotel Service into a single, unified response.
    
7.  **API Gateway** – This aggregated response is sent back to the API Gateway, which then forwards it to the client.
    

![](https://cdn.hashnode.com/uploads/covers/679f94da85491cdb2d8e2e57/22ebd333-fff7-4359-95f1-f0d295b425f1.png align="center")

* * *

## Approaches to Implement an API Gateway

| Category | Examples | Where it runs | Configuration |
| --- | --- | --- | --- |
| **1\. Libraries / Frameworks** | Flask, FastAPI, Spring Cloud Gateway, Express middleware | Inside your application | Usually code-based |
| **2\. Self-hosted Gateway Tools** | Kong, Envoy, Traefik, NGINX | Separate gateway process/container | Often YAML/config/API |
| **3\. Managed API Gateway Services** | AWS API Gateway, Azure API Management, Google API Gateway | Cloud provider | Cloud console/API/IaC |

> **1\. Libraries / Frameworks**

*   Here, **<mark class="bg-yellow-200 dark:bg-yellow-500/30">you build the gateway yourself</mark>** using an application framework.
    
*   This gives you **<mark class="bg-yellow-200 dark:bg-yellow-500/30">maximum flexibility</mark>**, as you are responsible for things such as: Routing, Load balancing, Service discovery, Authentication, Rate limiting, Retries, Circuit breaking, Logging, Metrics and Request/response transformation.
    

> **2\. Self-hosted API Gateway / Proxy Tools**

*   Here, the gateway is a **<mark class="bg-yellow-200 dark:bg-yellow-500/30">separate infrastructure component</mark>**.
    
*   Your applications don't need to implement gateway functionality.
    

> **3\. Managed API Gateway Services**

*   Here, **<mark class="bg-yellow-200 dark:bg-yellow-500/30">you don't operate the gateway infrastructure yourself</mark>.**
    
*   **<mark class="bg-yellow-200 dark:bg-yellow-500/30">Cloud provider manages much of the underlying gateway infrastructure</mark>.** You just configure things such as: Routes, Authentication, Throttling, Authorization, Integrations, Custom domains and Monitoring.
    

* * *

## Code Example

Here, I have implemented API Gateway using Flask.

### User Service (running at `192.168.1.36:5001`)

![](https://cdn.hashnode.com/uploads/covers/679f94da85491cdb2d8e2e57/e08fb923-54d2-4770-8883-837c2c127d3e.png align="center")

`main.py`

```python
# ================================================================
# Workaround to resolve .consul domain names using Consul's DNS resolver (127.0.0.1:8600) instead of the OS's default resolver (8.8.8.8:53). 
# This is necessary because the OS's resolver may not be able to resolve .consul domain names, which are used for service discovery in a Consul environment.
# ================================================================
import socket
import dns.resolver

_consul_resolver = dns.resolver.Resolver(configure=False)
_consul_resolver.nameservers = ["127.0.0.1"]
_consul_resolver.port = 8600

_original_getaddrinfo = socket.getaddrinfo

def patched_getaddrinfo(host, *args, **kwargs):
    if host.endswith(".consul"):
        answer = _consul_resolver.resolve(host, "A")
        host = str(answer[0])
    return _original_getaddrinfo(host, *args, **kwargs)

socket.getaddrinfo = patched_getaddrinfo
# ================================================================
# ================================================================
# ================================================================


from flask import Flask, request
from clients.rating_client import RatingClient
from clients.hotel_client import HotelClient


app = Flask(__name__)

db = [
        {
            "userId": "user-1",
            "name": "John Doe",
            "email": "john@gmail.com",
            "about": "I am a software engineer"
        },
        {
            "userId": "user-2",
            "name": "Randy Smith",
            "email": "randy@gmail.com",
            "about": "I am a doctor"
        },
        {
            "userId": "user-3",
            "name": "Shubham Agrawal",
            "email": "shubham@gmail.com",
            "about": "I am a data scientist"
        }
    ]


@app.route("/health", methods=["GET"])
def health():
    return "Welcome to User Microservice"


@app.route("/users", methods=["POST"])
def create_user():
    # fetch name, email & about from request body
    name = request.json.get("name")
    email = request.json.get("email")
    about = request.json.get("about")

    created_user = {
        "userId": "user-1",
        "name": name,
        "email": email,
        "about": about
    }

    return created_user, 201


@app.route("/users", methods=["GET"])
def get_all_users():
    # fetch all users from database
    users = db

    # ===============================================
    # fetch ratings from rating-service for each user
    # ===============================================
    rating_client = RatingClient()

    hotel_client = HotelClient()

    for user in users:
        # Service discovery + Communication in one go
        ratings = rating_client.get_all_ratings_by_user(user_id=user['userId'])

        # ===================================================
        # fetch hotel info from hotel-service for each rating
        # ===================================================
        for rating in ratings:
            # Service discovery + Communication in one go
            hotel = hotel_client.get_hotel(hotel_id=rating['hotelId'])

            rating["hotel"] = hotel

        user["ratings"] = ratings
    
    return users, 200


@app.route("/users/<userId>", methods=["GET"])
def get_user(userId):
    # fetch user from database using userId
    for user in db:
        if user["userId"] == userId:
            return user, 200


@app.route("/users/<userId>", methods=["PUT"])
def update_user(userId):
    return "Updated user with userId: {}".format(userId)


@app.route("/users/<userId>", methods=["DELETE"])
def delete_user(userId):
    return "Deleted user with userId: {}".format(userId)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001, debug=True)
```

`clients/hotel_client.py`

```python
import pyfeign
from pyfeign import Config, Body, Path
from typing import Any, Dict, List


@pyfeign.Pyfeign(config=Config(base_url="http://hotel-service.service.consul:5002"))
class HotelClient:

    @pyfeign.get("/health")
    def health(self) -> str:
        """Check Hotel Service health"""
        pass

    @pyfeign.post("/hotels")
    def create_hotel(self, body: Dict[str, Any] = Body()) -> Dict[str, Any]:
        """Create a new hotel"""
        pass

    @pyfeign.get("/hotels")
    def get_all_hotels(self) -> List[Dict[str, Any]]:
        """Get all hotels"""
        pass

    @pyfeign.get("/hotels/{hotel_id}")
    def get_hotel(self, hotel_id: str = Path()) -> Dict[str, Any]:
        """Get a single hotel by ID"""
        pass

    @pyfeign.put("/hotels/{hotel_id}")
    def update_hotel(self, hotel_id: str = Path()) -> str:
        """Update a hotel by ID"""
        pass

    @pyfeign.delete("/hotels/{hotel_id}")
    def delete_hotel(self, hotel_id: str = Path()) -> str:
        """Delete a hotel by ID"""
        pass
```

`clients/rating_client.py`

```python
import pyfeign
from pyfeign import Config, Body, Path
from typing import Any, Dict, List


@pyfeign.Pyfeign(config=Config(base_url="http://rating-service.service.consul:5003"))
class RatingClient:

    @pyfeign.get("/health")
    def health(self) -> str:
        """Check Rating Service health"""
        pass

    @pyfeign.post("/ratings")
    def create_rating(self, body: Dict[str, Any] = Body()) -> Dict[str, Any]:
        """Create a new rating"""
        pass

    @pyfeign.get("/ratings")
    def get_all_ratings(self) -> List[Dict[str, Any]]:
        """Get all ratings"""
        pass

    @pyfeign.get("/ratings/user/{user_id}")
    def get_all_ratings_by_user(self, user_id: str = Path()) -> List[Dict[str, Any]]:
        """Get all ratings for a given user (each rating includes hotel info)"""
        pass

    @pyfeign.get("/ratings/hotel/{hotel_id}")
    def get_all_ratings_by_hotel(self, hotel_id: str = Path()) -> List[Dict[str, Any]]:
        """Get all ratings for a given hotel"""
        pass
```

### Hotel Service (running at `192.168.1.36:5002`)

![](https://cdn.hashnode.com/uploads/covers/679f94da85491cdb2d8e2e57/ddb713b4-509f-447c-a38f-5138a2b7032d.png align="center")

`main.py`

```python
from flask import Flask, request

app = Flask(__name__)


db = [
        {
            "hotelId": "hotel-1",
            "name": "Radisson Blue",
            "location": "Noida, UP",
            "about": "Very professional staff and very clean rooms."
        },
        {
            "hotelId": "hotel-2",
            "name": "Hayat Residency",
            "location": "Lucknow, UP",
            "about": "Very good food service."
        },
        {
            "hotelId": "hotel-3",
            "name": "Brijwasi Lands Inn",
            "location": "Mathura, UP",
            "about": "Nice for marriage ceremonies."
        }
    ]


@app.route("/health", methods=["GET"])
def health():
    return "Welcome to Hotel Microservice"


@app.route("/hotels", methods=["POST"])
def create_hotel():
    # fetch name, location & about from request body
    name = request.json.get("name")
    location = request.json.get("location")
    about = request.json.get("about")

    created_hotel = {
        "hotelId": "hotel-1",
        "name": name,
        "location": location,
        "about": about
    }

    return created_hotel, 201


@app.route("/hotels", methods=["GET"])
def get_all_hotels():
    # fetch all hotels from database
    hotels = db
    return hotels, 200


@app.route("/hotels/<hotelId>", methods=["GET"])
def get_hotel(hotelId):
    # fetch hotel from database using hotelId
    for hotel in db:
        if hotel["hotelId"] == hotelId:
            return hotel, 200


@app.route("/hotels/<hotelId>", methods=["PUT"])
def update_hotel(hotelId):
    return "Updated hotel with hotelId: {}".format(hotelId)


@app.route("/hotels/<hotelId>", methods=["DELETE"])
def delete_user(hotelId):
    return "Deleted hotel with hotelId: {}".format(hotelId)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5002, debug=True)
```

### Rating Service (running at `192.168.1.36:5003`)

![](https://cdn.hashnode.com/uploads/covers/679f94da85491cdb2d8e2e57/52062356-e405-438e-a7ce-22ab3e8fff18.png align="center")

`main.py`

```python
from flask import Flask, request


app = Flask(__name__)

db = [
    {
        "ratingId": "rating-1",
        "userId": "user-1",
        "hotelId": "hotel-1",
        "rating": 8,
        "feedback": "Great hotel with excellent service.",
    },
    {
        "ratingId": "rating-2",
        "userId": "user-1",
        "hotelId": "hotel-2",
        "rating": 6,
        "feedback": "Needs improvement in food quality. Space-wise, it is good.",
    },
    {
        "ratingId": "rating-3",
        "userId": "user-1",
        "hotelId": "hotel-3",
        "rating": 4,
        "feedback": "Not recommended at all.",
    },
    {
        "ratingId": "rating-4",
        "userId": "user-2",
        "hotelId": "hotel-1",
        "rating": 6,
        "feedback": "Needs improvement in food quality. Space-wise, it is good.",
    },
    {
        "ratingId": "rating-5",
        "userId": "user-2",
        "hotelId": "hotel-2",
        "rating": 4,
        "feedback": "Not recommended at all.",
    },
    {
        "ratingId": "rating-6",
        "userId": "user-3",
        "hotelId": "hotel-3",
        "rating": 4,
        "feedback": "Not recommended at all.",
    },
]


@app.route("/health", methods=["GET"])
def health():
    return "Welcome to Rating Microservice"


@app.route("/ratings", methods=["POST"])
def create_rating():
    # fetch userId, hotelId, rating & feedback from request body
    userId = request.json.get("userId")
    hotelId = request.json.get("hotelId")
    rating = request.json.get("rating")
    feedback = request.json.get("feedback")

    created_rating = {
        "ratingId": "rating-1",
        "userId": userId,
        "hotelId": hotelId,
        "rating": rating,
        "feedback": feedback
    }

    return created_rating, 201


@app.route("/ratings", methods=["GET"])
def get_all_ratings():
    # fetch all ratings from database
    ratings = db
        
    return ratings, 200


@app.route("/ratings/user/<userId>", methods=["GET"])
def get_all_ratings_by_user(userId):
    # fetch all ratings from database using userId
    all_ratings = []
    for rating in db:
        if rating["userId"] == userId:
            all_ratings.append(rating)

    return all_ratings, 200


@app.route("/ratings/hotel/<hotelId>", methods=["GET"])
def get_all_ratings_by_hotel(hotelId):
    # fetch all ratings from database using hotelId
    all_ratings = []
    for rating in db:
        if rating["hotelId"] == hotelId:
            all_ratings.append(rating)

    return all_ratings, 200


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5003, debug=True)
```

### API Gateway (running at `192.168.1.36:5004`)

![](https://cdn.hashnode.com/uploads/covers/679f94da85491cdb2d8e2e57/d7441ab6-6fb3-43f1-afee-4038669a138e.png align="center")

`gateway.json`

```json
{
  "routes": [
    {
      "path": "/users",
      "service": "user-service"
    },
    {
      "path": "/hotels",
      "service": "hotel-service"
    },
    {
      "path": "/ratings",
      "service": "rating-service"
    }
  ]
}
```

`main.py`

```python
# ================================================================
# Workaround to resolve .consul domain names using Consul's DNS resolver (127.0.0.1:8600) instead of the OS's default resolver (8.8.8.8:53). 
# This is necessary because the OS's resolver may not be able to resolve .consul domain names, which are used for service discovery in a Consul environment.
# ================================================================
import socket
import dns.resolver

_consul_resolver = dns.resolver.Resolver(configure=False)
_consul_resolver.nameservers = ["127.0.0.1"]
_consul_resolver.port = 8600

_original_getaddrinfo = socket.getaddrinfo

def patched_getaddrinfo(host, *args, **kwargs):
    if host.endswith(".consul"):
        answer = _consul_resolver.resolve(host, "A")
        host = str(answer[0])
    return _original_getaddrinfo(host, *args, **kwargs)

socket.getaddrinfo = patched_getaddrinfo
# ================================================================
# ================================================================
# ================================================================



import json
from pathlib import Path
import requests
from flask import Flask, Response, request


app = Flask(__name__)

BASE_DIR = Path(__file__).resolve().parent

with open(BASE_DIR / "gateway.json") as file:
    config = json.load(file)

ROUTES = config["routes"]


def find_route(path):
    for route in ROUTES:
        route_path = route["path"]

        if path == route_path or path.startswith(route_path + "/"):
            return route

    return None


def resolve_service(service_name):
    """
    Query Consul DNS for a healthy instance of service_name.
    Returns (host_or_ip, port).
    """
    query = f"{service_name}.service.consul"
    answer = _consul_resolver.resolve(query, "SRV")
    srv = answer[0]  # pick first; randomize/round-robin for load balancing
    target_host = str(srv.target).rstrip(".")
    port = srv.port

    # SRV gives you a hostname, not necessarily an IP — resolve it too
    a_answer = _consul_resolver.resolve(target_host, "A")
    ip = str(a_answer[0])

    return ip, port


def forward_request(route):

    service_name = route["service"]
    host, port = resolve_service(service_name)

    target_url = f"http://{host}:{port}{request.path}"

    response = requests.request(
        method=request.method,
        url=target_url,
        params=request.args,
        headers={
            key: value
            for key, value in request.headers
            if key.lower() != "host"
        },
        data=request.get_data(),
        allow_redirects=False,
        timeout=10
    )

    excluded_headers = {
        "content-length",
        "connection",
        "transfer-encoding"
    }

    response_headers = [
        (key, value)
        for key, value in response.headers.items()
        if key.lower() not in excluded_headers
    ]

    return Response(
        response.content,
        status=response.status_code,
        headers=response_headers
    )


@app.route("/<path:path>", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
def gateway(path):

    route = find_route("/" + path)

    if route is None:
        return {
            "error": "Route not found",
            "path": request.path
        }, 404

    try:

        return forward_request(route)

    except requests.exceptions.ConnectionError:
        return {
            "error": "Service unavailable",
            "service": route["service"]
        }, 503

    except requests.exceptions.Timeout:
        return {
            "error": "Service timeout",
            "service": route["service"]
        }, 504


@app.route("/health")
def health():
    return {"status": "UP"}, 200


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5004, debug=True)
```

Instead of client calling each service, for example

*   `GET` `http://192.168.1.36:5001/users`
    
*   `GET` `http://192.168.1.36:5002/hotels`
    
*   `GET` `http://192.168.1.36:5003/ratings`
    

client can only call the API Gateway, and API Gateway intelligently routes them to the appropriate microservice. For example

*   `GET` `http://192.168.1.36:5004/users`
    
*   `GET` `http://192.168.1.36:5004/hotels`
    
*   `GET` `http://192.168.1.36:5004/ratings`
    

* * *

Happy learning!
