1
0
mirror of https://github.com/go-micro/go-micro.git synced 2025-11-23 21:44:41 +02:00
Files
go-micro/internal/website/docs/examples/realworld/api-gateway.md
2025-11-13 18:34:40 +00:00

8.9 KiB

layout
layout
default

API Gateway with Backend Services

A complete example showing an API gateway routing to multiple backend microservices.

Architecture

                  ┌─────────────┐
   Client ───────>│ API Gateway │
                  └──────┬──────┘
                         │
          ┌──────────────┼──────────────┐
          │              │              │
    ┌─────▼────┐   ┌────▼─────┐  ┌────▼─────┐
    │  Users   │   │  Orders  │  │ Products │
    │ Service  │   │ Service  │  │ Service  │
    └──────────┘   └──────────┘  └──────────┘
          │              │              │
          └──────────────┼──────────────┘
                         │
                  ┌──────▼──────┐
                  │  PostgreSQL │
                  └─────────────┘

Services

1. Users Service

// services/users/main.go
package main

import (
    "context"
    "database/sql"
    "go-micro.dev/v5"
    "go-micro.dev/v5/server"
    _ "github.com/lib/pq"
)

type User struct {
    ID    int64  `json:"id"`
    Email string `json:"email"`
    Name  string `json:"name"`
}

type UsersService struct {
    db *sql.DB
}

type GetUserRequest struct {
    ID int64 `json:"id"`
}

type GetUserResponse struct {
    User *User `json:"user"`
}

func (s *UsersService) Get(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
    var u User
    err := s.db.QueryRow("SELECT id, email, name FROM users WHERE id = $1", req.ID).
        Scan(&u.ID, &u.Email, &u.Name)
    if err != nil {
        return err
    }
    rsp.User = &u
    return nil
}

func main() {
    db, err := sql.Open("postgres", "postgres://user:pass@localhost/users?sslmode=disable")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    svc := micro.NewService(
        micro.Name("users"),
        micro.Version("1.0.0"),
    )

    svc.Init()

    server.RegisterHandler(svc.Server(), &UsersService{db: db})

    if err := svc.Run(); err != nil {
        panic(err)
    }
}

2. Orders Service

// services/orders/main.go
package main

import (
    "context"
    "database/sql"
    "time"
    "go-micro.dev/v5"
    "go-micro.dev/v5/client"
    "go-micro.dev/v5/server"
)

type Order struct {
    ID        int64     `json:"id"`
    UserID    int64     `json:"user_id"`
    ProductID int64     `json:"product_id"`
    Amount    float64   `json:"amount"`
    Status    string    `json:"status"`
    CreatedAt time.Time `json:"created_at"`
}

type OrdersService struct {
    db     *sql.DB
    client client.Client
}

type CreateOrderRequest struct {
    UserID    int64   `json:"user_id"`
    ProductID int64   `json:"product_id"`
    Amount    float64 `json:"amount"`
}

type CreateOrderResponse struct {
    Order *Order `json:"order"`
}

func (s *OrdersService) Create(ctx context.Context, req *CreateOrderRequest, rsp *CreateOrderResponse) error {
    // Verify user exists
    userReq := s.client.NewRequest("users", "UsersService.Get", &struct{ ID int64 }{ID: req.UserID})
    userRsp := &struct{ User interface{} }{}
    if err := s.client.Call(ctx, userReq, userRsp); err != nil {
        return err
    }

    // Verify product exists
    prodReq := s.client.NewRequest("products", "ProductsService.Get", &struct{ ID int64 }{ID: req.ProductID})
    prodRsp := &struct{ Product interface{} }{}
    if err := s.client.Call(ctx, prodReq, prodRsp); err != nil {
        return err
    }

    // Create order
    var o Order
    err := s.db.QueryRow(`
        INSERT INTO orders (user_id, product_id, amount, status, created_at)
        VALUES ($1, $2, $3, $4, $5)
        RETURNING id, user_id, product_id, amount, status, created_at
    `, req.UserID, req.ProductID, req.Amount, "pending", time.Now()).
        Scan(&o.ID, &o.UserID, &o.ProductID, &o.Amount, &o.Status, &o.CreatedAt)

    if err != nil {
        return err
    }

    rsp.Order = &o
    return nil
}

func main() {
    db, err := sql.Open("postgres", "postgres://user:pass@localhost/orders?sslmode=disable")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    svc := micro.NewService(
        micro.Name("orders"),
        micro.Version("1.0.0"),
    )

    svc.Init()

    server.RegisterHandler(svc.Server(), &OrdersService{
        db:     db,
        client: svc.Client(),
    })

    if err := svc.Run(); err != nil {
        panic(err)
    }
}

3. API Gateway

// gateway/main.go
package main

import (
    "encoding/json"
    "net/http"
    "strconv"
    "go-micro.dev/v5"
    "go-micro.dev/v5/client"
)

type Gateway struct {
    client client.Client
}

func (g *Gateway) GetUser(w http.ResponseWriter, r *http.Request) {
    idStr := r.URL.Query().Get("id")
    id, err := strconv.ParseInt(idStr, 10, 64)
    if err != nil {
        http.Error(w, "invalid id", http.StatusBadRequest)
        return
    }

    req := g.client.NewRequest("users", "UsersService.Get", &struct{ ID int64 }{ID: id})
    rsp := &struct{ User interface{} }{}

    if err := g.client.Call(r.Context(), req, rsp); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(rsp)
}

func (g *Gateway) CreateOrder(w http.ResponseWriter, r *http.Request) {
    var body struct {
        UserID    int64   `json:"user_id"`
        ProductID int64   `json:"product_id"`
        Amount    float64 `json:"amount"`
    }

    if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
        http.Error(w, "invalid request", http.StatusBadRequest)
        return
    }

    req := g.client.NewRequest("orders", "OrdersService.Create", body)
    rsp := &struct{ Order interface{} }{}

    if err := g.client.Call(r.Context(), req, rsp); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(rsp)
}

func main() {
    svc := micro.NewService(
        micro.Name("api.gateway"),
    )
    svc.Init()

    gw := &Gateway{client: svc.Client()}

    http.HandleFunc("/users", gw.GetUser)
    http.HandleFunc("/orders", gw.CreateOrder)

    http.ListenAndServe(":8080", nil)
}

Running the Example

Development (Local)

# Terminal 1: Users service
cd services/users
go run main.go

# Terminal 2: Products service
cd services/products
go run main.go

# Terminal 3: Orders service
cd services/orders
go run main.go

# Terminal 4: API Gateway
cd gateway
go run main.go

Testing

# Get user
curl http://localhost:8080/users?id=1

# Create order
curl -X POST http://localhost:8080/orders \
  -H 'Content-Type: application/json' \
  -d '{"user_id": 1, "product_id": 100, "amount": 99.99}'

Docker Compose

version: '3.8'

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"

  users:
    build: ./services/users
    environment:
      MICRO_REGISTRY: nats
      MICRO_REGISTRY_ADDRESS: nats://nats:4222
      DATABASE_URL: postgres://postgres:secret@postgres/users
    depends_on:
      - postgres
      - nats

  products:
    build: ./services/products
    environment:
      MICRO_REGISTRY: nats
      MICRO_REGISTRY_ADDRESS: nats://nats:4222
      DATABASE_URL: postgres://postgres:secret@postgres/products
    depends_on:
      - postgres
      - nats

  orders:
    build: ./services/orders
    environment:
      MICRO_REGISTRY: nats
      MICRO_REGISTRY_ADDRESS: nats://nats:4222
      DATABASE_URL: postgres://postgres:secret@postgres/orders
    depends_on:
      - postgres
      - nats

  gateway:
    build: ./gateway
    ports:
      - "8080:8080"
    environment:
      MICRO_REGISTRY: nats
      MICRO_REGISTRY_ADDRESS: nats://nats:4222
    depends_on:
      - users
      - products
      - orders

  nats:
    image: nats:latest
    ports:
      - "4222:4222"

Run with:

docker-compose up

Key Patterns

  1. Service isolation: Each service owns its database
  2. Service communication: Via Go Micro client
  3. Gateway pattern: Single entry point for clients
  4. Error handling: Proper HTTP status codes
  5. Registry: mDNS for local, NATS for Docker

Production Considerations

  • Add authentication/authorization
  • Implement request tracing
  • Add circuit breakers for service calls
  • Use connection pooling
  • Add rate limiting
  • Implement proper logging
  • Use health checks
  • Add metrics collection

See Production Patterns for more details.