Implementing a basic API Gateway from scratch

Parth GuptaParth Gupta
7 min read

In today’s software ecosystem, API Gateways play a crucial role in managing and securing communication between clients and microservices. Whether you are building a large-scale distributed system or just starting your journey into backend development, understanding API Gateways is essential.

In this blog, we will dive into the concept of API Gateways, why they are needed, and how to implement a simple one using Node.js and Express. By the end, you’ll have both a solid theoretical foundation and a hands-on example you can extend in your own projects.

Pre-requisites before reading this blog

This blog is designed for beginners, but having a basic understanding of the following will help you get the most out of it:

  • JavaScript fundamentals (variables, functions, modules)

  • Basic knowledge of Node.js and Express.js

  • A working setup of Node.js on your machine

Contents of this blog

  1. What is an API Gateway?

  2. Why Do We Need an API Gateway?

  3. Common Features of an API Gateway

  4. Implementing a basic API Gateway

  5. Conclusion(What should you do now?)

What is an API Gateway?

As modern applications grow in scale and complexity, monolithic architectures are giving way to microservices, where each service handles a specific responsibility independently. While this approach improves scalability and maintainability, it introduces a new challenge: how should clients interact with multiple services efficiently?

That’s where API Gateways step in. An API Gateway acts as a single entry point for all client requests, streamlining communication with backend services. It takes care of authentication, routing, rate limiting, logging, and more, allowing developers to focus on building services rather than worrying about integration complexity.

Why Do We Need an API Gateway?

  1. Centralized Entry Point
    Without a gateway, clients would need to know and manage URLs for every service (users, orders, products, payments, etc.). The gateway provides one single endpoint (e.g., api.example.com) that hides this complexity.

  2. Security
    Gateways handle authentication and authorization before requests even reach your services. This ensures that only valid and authorized requests pass through.

  3. Request Routing
    A gateway knows where to send incoming requests, user-related requests go to the user service, order-related requests to the order service, and so on.

  4. Performance Enhancements
    Features like caching, rate limiting, and load balancing can be implemented at the gateway, improving the overall performance and reliability of your system.

  5. Cross-Cutting Concerns
    Instead of implementing logging, error handling, or monitoring in every service, the gateway can handle these common concerns in one place.

Common Features of an API Gateway

  • Authentication & Authorization – Ensuring only legitimate users access the system.

  • Request Validation – Checking if the client request is complete and valid.

  • Rate Limiting – Preventing clients from spamming your APIs.

  • Load Balancing – Distributing traffic across multiple instances of a service.

  • Caching – Storing frequent responses to reduce load.

  • CORS Handling – Enabling safe communication between your APIs and web apps.

Implementing a basic API Gateway

  1. Create a new folder and initialize a Node.js project:

mkdir api-gateway
cd api-gateway
npm init -y
  1. Install Dependencies

{
  "name": "apigateway",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "axios": "^1.11.0",
    "cors": "^2.8.5",
    "dotenv": "^17.2.1",
    "express": "^5.1.0",
    "jsonwebtoken": "^9.0.2"
  },
  "devDependencies": {
    "nodemon": "^3.1.10"
  }
}
  1. This will be our folder structure.

  2. Now let’s write our app.js

require('dotenv').config();
const express = require("express");
const routes = require("./routes");
const logger = require("./middleware/logger");
const auth = require("./middleware/auth");
const { errorHandler } = require("./utils/errorHandler");

const app = express();

// middleware pipeline
app.use(express.json());          // parse JSON
app.use(logger);                  // log every request
app.use(auth);                    // auth check
app.use("/", routes);             // load routes
app.use(errorHandler);            // global error handler

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

This code sets up the entry point of our API Gateway. It loads environment variables, initializes an Express app, and wires up a middleware pipeline for JSON parsing, logging, authentication, and error handling. Finally, it mounts the routes and starts the server on the configured port. Now let’s code the individual components of this pipeline.

  1. Writing logger middleware

// middleware/logger.js
module.exports = (req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next();
};

This middleware logs every request with timestamp.

  1. Writing auth middleware

require('dotenv').config();
const jwt = require("jsonwebtoken");


module.exports = (req, res, next) => {
  const token = req.headers["authorization"];
  if (!token) return res.status(401).json({ error: "No token provided" });

  jwt.verify(token.replace("Bearer ", ""), `${process.env.JWT_SECRET_KEY}`, (err, decoded) => {
    if (err) return res.status(401).json({ error: "Invalid token" });
    req.user = decoded;
    next();
  });
};

This middleware checks checks if the request has an Authorization header with a valid JWT. If the token is missing or invalid, it returns a 401 Unauthorized response. If valid, it attaches the decoded user info to req.user and passes control to the next middleware or route.

  1. Writing the routes

const express = require("express");
const { getUser } = require("../services/user");
const { getOrder } = require("../services/order");

const router = express.Router();

// user service
router.get("/users/:id", async (req, res, next) => {
  try {
    const data = await getUser(req.params.id);
    res.json(data);
  } catch (err) {
    next(err);
  }
});

// order service
router.get("/orders/:id", async (req, res, next) => {
  try {
    const data = await getOrder(req.params.id);
    res.json(data);
  } catch (err) {
    next(err);
  }
});

module.exports = router;

This code defines an Express router that exposes two API endpoints:

  • GET /users/:id → fetches user data by calling getUser() service.

  • GET /orders/:id → fetches order data by calling getOrder() service.

Both routes handle errors using try/catch and pass them to the global error handler with next(err).

  1. Writing the services

// services/order.js
async function getOrder(id){
    return{
        id,
        item:"Laptop",
        price: 1200, 
        status: "Shipped"
    }
}
module.exports = {getOrder};

//services/user.js
async function getUser(id) {
  return {
    id,
    name: "John Doe",
    email: "john@example.com"
  };
}
module.exports = { getUser };

These two files (order.js and user.js) are dummy services created just for learning purposes. Instead of fetching real data from a database or API, they simply return hardcoded JSON objects:

  • getOrder(id) → returns an order with fixed details (Laptop, price, shipped status).

  • getUser(id) → returns a user with fixed details (John Doe, email).

Notice how there’s no Express import here — that’s a good design choice because it keeps these service functions pure and independent. They only handle business logic (returning data), while Express code is kept in the router. This separation makes the project cleaner, easier to maintain, and more testable.

  1. When the response is successful, the client receives the expected data object. But things don’t always go perfectly—what if something goes wrong in the system, or even something simple like your Wi-Fi disconnecting? In such cases, we need to handle errors properly to ensure the application doesn’t just break unexpectedly.

  2. Writing a errorHandler

    // utils/errorHandler.js
    module.exports.errorHandler = (err, req, res, next) => {
      console.error(err);
      res.status(500).json({ error: err.message || "Internal Server Error" });
    };
    

    This code sets up a centralized error handler for the Express app. Whenever something goes wrong in the system, instead of crashing or exposing raw errors, the middleware catches it, logs the issue, and sends back a proper JSON response with a 500 status code. This ensures the client always receives a clean and predictable error message, making the application more reliable and user-friendly.

And that’s it! You have implemented a basic API Gateway.

Conclusion(What should you do now?)

In this blog, we built a basic API Gateway using Express. We structured our project with routes, services, and middleware, and even added a centralized error handler to make the system more reliable. While the example was simple, it reflects how real-world gateways are designed, keeping logic cleanly separated and easy to scale.

This foundation is important because an API Gateway often becomes the front door of your entire system. If it’s messy or insecure, every downstream service suffers. By starting with a clean structure, you set yourself up for better security, maintainability, and scalability as your project grows.

Now that you’ve got the basics in place, here are some things you can try:

  • Add a new endpoint (e.g., /products/:id) to simulate a real microservice.

  • Modify the auth middleware to check user roles and permissions.

  • Add request validation for incoming data to ensure only clean inputs pass through.

  • Implement rate limiting middleware to protect your services from abuse.

  • Enable CORS configuration so web clients can interact safely with your API.

Each of these additions will bring your API Gateway closer to a production-ready system.

End of the Blog.

If you want notifications about upcoming blogs, you can subscribe and follow me (Parth Gupta) on Hashnode.

You can connect with me on:

1
Subscribe to my newsletter

Read articles from Parth Gupta directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Parth Gupta
Parth Gupta