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/httppackage - 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:
- Client sends request → Gateway receives HTTP request on configured port
- Router matches route → Finds the route configuration based on method and path
- Middleware executes → Each middleware processes the request (auth, rate limit, etc.)
- Load balancer selects target → Picks a backend instance using the configured strategy
- Proxy forwards request → Sends the request to the selected backend
- 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:
- How to use Go's
net/httppackage to build HTTP servers and reverse proxies - The middleware pattern and how to implement a flexible middleware system
- Load balancing strategies like round-robin distribution
- Token bucket algorithm for rate limiting
- JWT authentication basics in Go
- YAML configuration for route definitions
- 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.