Introduction to API Gateway

Introduction to API Gateway

In this tutorial, you'll build an API Gateway from scratch using Go. By the end, you'll have a fully functional gateway that can route requests, balance load across multiple backends, authenticate users, and rate-limit traffic.

The complete source code for this tutorial is available on GitHub: github.com/mohitkumar/api-gateway

What is an API Gateway?

An API Gateway acts as a single entry point for all client requests to your backend services. Instead of clients communicating directly with multiple microservices, they send all requests to the gateway, which then routes them to the appropriate service.

Here's what our gateway will handle:

  • Routing - Match incoming requests to configured backend services
  • Load Balancing - Distribute traffic across multiple instances of a service
  • Authentication - Validate JWT tokens before forwarding requests
  • Rate Limiting - Protect backends from being overwhelmed
  • Request/Response Transformation - Modify headers and query parameters

Project Structure

Let's start by understanding how we'll organize our code. A well-structured project makes the gateway easier to maintain and extend.

mgateway/
├── main.go                  # Application entry point
├── cmd/
│   ├── root.go              # CLI commands and server startup
│   └── config.go            # CLI configuration struct
├── config/
│   └── config.go            # YAML configuration types
├── router/
│   └── route.go             # HTTP routing logic
├── proxy/
│   └── proxy.go             # Reverse proxy implementation
├── lb/
│   └── lb.go                # Load balancer
├── middleware/
│   ├── middleware.go        # Middleware system & registry
│   ├── auth.go              # JWT authentication
│   ├── ratelimit.go         # Rate limiting middleware
│   └── transform.go         # Request/response transformation
├── ratelimit/
│   └── ratelimit.go         # Token bucket rate limiter
├── transform/
│   └── transformer.go       # Transformation logic
├── config.yaml              # Route configuration file
├── go.mod
└── Makefile

Each package has a single responsibility:

Package Responsibility
main.go Application entry point
cmd CLI setup, configuration loading, server startup
config YAML configuration type definitions
router Route matching and request dispatching
proxy HTTP reverse proxy to backend services
lb Load balancing across multiple targets
middleware Pluggable middleware system
ratelimit Token bucket rate limiter implementation
transform Request/response header and query manipulation

Prerequisites

Before we begin, make sure you have:

  • Go 1.21+ installed
  • Basic understanding of Go's net/http package
  • A text editor or IDE

Setting Up the Project

Let's initialize the Go module and create our directory structure:

# Create project directory
mkdir mgateway && cd mgateway

# Initialize Go module
go mod init github.com/yourusername/mgateway

# Create directory structure
mkdir -p cmd/gateway config router proxy lb middleware ratelimit transform

Dependencies

Our gateway uses a few external packages. Add them to your go.mod:

module github.com/yourusername/mgateway

go 1.21

require (
    github.com/golang-jwt/jwt/v5 v5.3.0
    github.com/spf13/cobra v1.10.2
    github.com/spf13/viper v1.21.0
    go.yaml.in/yaml/v3 v3.0.4
)

Install the dependencies:

go mod tidy

Here's what each dependency provides:

Package Purpose
golang-jwt/jwt JWT token parsing and validation
spf13/cobra Command-line interface framework
spf13/viper Configuration management
yaml/v3 YAML parsing for configuration files

How the Gateway Works

Before diving into the code, let's understand the request flow through our gateway:

  1. Client sends request → Gateway receives HTTP request on configured port
  2. Router matches route → Finds the route configuration based on method and path
  3. Middleware executes → Each middleware processes the request (auth, rate limit, etc.)
  4. Load balancer selects target → Picks a backend instance using the configured strategy
  5. Proxy forwards request → Sends the request to the selected backend
  6. Response returns → Backend response flows back through middleware to the client

Each component is built as an http.Handler, which allows them to be composed together using Go's standard interfaces.

Setting Up Test Infrastructure

We'll write tests as we build each component. Create the tests directory and a utility file with helper functions:

mkdir tests

Create tests/util.go:

package tests

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

// runMockUpstreamServer creates a mock backend that returns different 
// responses based on path and method
func runMockUpstreamServer(t *testing.T) *httptest.Server {
    t.Helper()
    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        if r.URL.Path == "/users" && r.Method == "GET" {
            w.Write([]byte(`{"users": [{"id": 1, "name": "Alice"}]}`))
        } else if r.URL.Path == "/users" && r.Method == "POST" {
            w.Write([]byte("Created"))
        }
    }))
}

// runMockUpstreamServerLB creates a mock backend that returns a specific 
// response (useful for testing load balancing)
func runMockUpstreamServerLB(t *testing.T, response string) *httptest.Server {
    t.Helper()
    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(response))
    }))
}

These helpers use Go's httptest package to create real HTTP servers for testing. We'll use them throughout the tutorial to test each component in isolation and together.

What You'll Learn

Throughout this tutorial, you'll learn:

  1. How to use Go's net/http package to build HTTP servers and reverse proxies
  2. The middleware pattern and how to implement a flexible middleware system
  3. Load balancing strategies like round-robin distribution
  4. Token bucket algorithm for rate limiting
  5. JWT authentication basics in Go
  6. YAML configuration for route definitions
  7. Testing patterns for HTTP handlers and middleware

Let's start building! In the next section, we'll design the architecture and understand how all the pieces fit together.