Example 07 - Remote Router (proxy.Router)

This example demonstrates how to use proxy.Router for quick, direct HTTP calls to external APIs without creating service wrappers. Perfect for simple integrations and one-off API calls.

๐Ÿ“‹ What Youโ€™ll Learn

๐Ÿ—๏ธ Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      Main App (:3001)                      โ”‚
โ”‚                                                            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  WeatherService                                      โ”‚  โ”‚
โ”‚  โ”‚  - GetWeatherReport()  โ†’ POST /weather-reports      โ”‚  โ”‚
โ”‚  โ”‚                                                      โ”‚  โ”‚
โ”‚  โ”‚  Uses: proxy.Router.DoJSON()                         โ”‚  โ”‚
โ”‚  โ”‚  No wrapper! Direct HTTP calls!                      โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                    โ”‚ Direct HTTP calls                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                     โ–ผ
     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
     โ”‚   Mock Weather API (:9001)                    โ”‚
     โ”‚   (Simulates OpenWeather, etc.)               โ”‚
     โ”‚                                               โ”‚
     โ”‚   GET /weather/{city}                         โ”‚
     โ”‚   GET /forecast/{city}?days=5                 โ”‚
     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key difference from Example 06:

๐Ÿš€ How to Run

Step 1: Start Mock Weather API

cd mock-weather-api
go run main.go

This starts the mock weather API on http://localhost:9001.

Step 2: Start Main Application

# From the example root directory
go run main.go

This starts the main application on http://localhost:3001.

Step 3: Test with HTTP Requests

Use the test.http file or curl:

# Get weather report (current only)
curl -X POST "http://localhost:3001/weather-reports?city=jakarta&forecast=false"

# Get weather report with forecast
curl -X POST "http://localhost:3001/weather-reports?city=jakarta&forecast=true&days=5"

# Different city
curl -X POST "http://localhost:3001/weather-reports?city=bandung&forecast=true&days=3"

๐Ÿ“‚ Project Structure

07-remote-router/
โ”œโ”€โ”€ main.go                           # Main application entry point
โ”œโ”€โ”€ config.yaml                       # Simple URL config
โ”œโ”€โ”€ test.http                         # HTTP test scenarios
โ”œโ”€โ”€ index                         # This file
โ”‚
โ”œโ”€โ”€ mock-weather-api/
โ”‚   โ””โ”€โ”€ main.go                       # Mock weather API
โ”‚
โ””โ”€โ”€ service/
    โ””โ”€โ”€ weather_service.go            # Service using proxy.Router

๐Ÿ”‘ Key Concepts

1. Simple URL Configuration

Store URL directly in service config - no special definitions needed:

service-definitions:
  weather-service:
    type: weather-service-factory
    config:
      weather-api-url: "http://localhost:9001"  # Direct URL

Why this is simpler:

Factory creates the router from URL:

func WeatherServiceFactory(deps map[string]any, config map[string]any) any {
    url := config["weather-api-url"].(string)
    return &WeatherService{
        weatherAPI: proxy.NewRemoteRouter(url),
    }
}

2. Service Using proxy.Router

Direct HTTP calls without wrapper:

type WeatherService struct {
    weatherAPI *proxy.Router
}

func (s *WeatherService) GetWeatherReport(p *GetWeatherReportParams) (*WeatherReport, error) {
    // Direct HTTP call - no wrapper!
    var current WeatherData
    err := s.weatherAPI.DoJSON(
        "GET",
        fmt.Sprintf("/weather/%s", p.City),
        nil,    // headers
        nil,    // request body
        &current, // response body
    )
    
    if err != nil {
        return nil, fmt.Errorf("failed to fetch weather: %w", err)
    }
    
    // Optionally fetch forecast
    if p.IncludeForecast {
        var forecast ForecastData
        err := s.weatherAPI.DoJSON(
            "GET",
            fmt.Sprintf("/forecast/%s?days=%d", p.City, p.ForecastDays),
            nil,
            nil,
            &forecast,
        )
        
        if err != nil {
            return nil, fmt.Errorf("failed to fetch forecast: %w", err)
        }
        
        report.Forecast = &forecast
    }
    
    return report, nil
}

Key points:

3. Factory Pattern

Create proxy.Router from URL in config:

func WeatherServiceFactory(deps map[string]any, config map[string]any) any {
    // Read URL from config
    url, ok := config["weather-api-url"].(string)
    if !ok {
        panic("weather-api-url is not a string")
    }
    
    // Create router directly
    return &WeatherService{
        weatherAPI: proxy.NewRemoteRouter(url),
    }
}

Simple instantiation:

4. Service Registration

Simple registration without route overrides:

lokstra_registry.RegisterServiceType("weather-service-factory",
    svc.WeatherServiceFactory, nil,
    deploy.WithResource("weather-report", "weather-reports"),
    deploy.WithConvention("rest"),
    // No route overrides needed!
)

Method names match REST convention:

๐ŸŽฏ proxy.Router API

Available Methods

  1. DoJSON - Most flexible (recommended)
    err := router.DoJSON(
     method string,        // "GET", "POST", etc.
     path string,          // "/weather/jakarta"
     headers map[string]string,
     requestBody any,      // nil for GET
     responseBody any,     // pointer to struct
    )
    
  2. Get - Simple GET requests
    resp, err := router.Get("/weather/jakarta", headers)
    
  3. PostJSON - POST with JSON body
    resp, err := router.PostJSON("/endpoint", data, headers)
    
  4. Serve - Low-level HTTP request
    resp, err := router.Serve(httpRequest)
    

๐Ÿ”„ Comparison: proxy.Router vs proxy.Service

Aspect proxy.Router (This Example) proxy.Service (Example 06)
Use Case Quick API access Structured services
Setup Minimal (just URL) Service wrapper + metadata
Convention โŒ Manual paths โœ… Auto-routing
Type Safety โœ… Response only โœ… Request + Response
Service Wrapper โŒ Not needed โœ… Required
Route Overrides N/A โœ… Supported
Best For One-off calls, prototyping Multi-endpoint services

When to Use proxy.Router

โœ… USE proxy.Router when:

When to Use proxy.Service

โœ… USE proxy.Service when:

๐Ÿ’ก Real-World Examples

Good Use Cases for proxy.Router

// Weather API
weatherRouter := proxy.NewRemoteRouter("https://api.weather.com")
weatherRouter.DoJSON("GET", "/current/jakarta", nil, nil, &weather)

// Currency Converter
currencyRouter := proxy.NewRemoteRouter("https://api.exchangerate.com")
currencyRouter.DoJSON("GET", "/latest?base=USD", nil, nil, &rates)

// IP Geolocation
ipRouter := proxy.NewRemoteRouter("https://ipapi.co")
ipRouter.DoJSON("GET", "/json", nil, nil, &location)

When to Upgrade to proxy.Service

If you find yourself:

Then: Create a proper service wrapper with proxy.Service (see Example 06).

๐ŸŽ“ Learning Points

1. Simplicity vs Structure Trade-off

proxy.Router: Simple, quick, less code

// One-liner!
router.DoJSON("GET", "/weather/jakarta", nil, nil, &weather)

proxy.Service: More code, better structure

// Typed method
weather, err := weatherService.GetWeather(&GetWeatherParams{
    City: "jakarta",
})

2. Manual URL Construction

With proxy.Router, you build URLs manually:

// Manual path construction
path := fmt.Sprintf("/forecast/%s?days=%d", city, days)
err := router.DoJSON("GET", path, nil, nil, &forecast)

With proxy.Service, framework does it:

// Framework builds URL from metadata + method name
forecast, err := service.GetForecast(&ForecastParams{
    City: city,
    Days: days,
})

3. Error Handling

Same pattern for both:

if err != nil {
    return nil, fmt.Errorf("failed to fetch data: %w", err)
}

Use proxy.ParseRouterError() for better error messages:

if err != nil {
    return nil, proxy.ParseRouterError(err)
}

4. Configuration Pattern

proxy.Router: Simple router definition

router-definitions:
  api-name:
    url: "https://api.example.com"

proxy.Service: Full service definition

external-service-definitions:
  api-name:
    url: "https://api.example.com"
    type: api-service-remote-factory

๐Ÿงช Mock Weather API

Built with Lokstra, demonstrates clean API design:

func getCurrentWeather(req *GetWeatherRequest) (*WeatherData, error) {
    weather, exists := mockWeather[req.City]
    if !exists {
        return nil, fmt.Errorf("weather data not found for city: %s", req.City)
    }
    
    result := *weather
    result.Timestamp = time.Now().Format(time.RFC3339)
    return &result, nil
}

func main() {
    r := lokstra.NewRouter("weather-api")
    
    r.GET("/weather/{city}", getCurrentWeather)
    r.GET("/forecast/{city}", getForecast)
    
    app := lokstra.NewApp("weather-api", ":9001", r)
    if err := app.Run(30 * time.Second); err != nil {
        fmt.Println("Error starting server:", err)
    }
}

Available cities: jakarta, bandung, surabaya

๐Ÿ”„ Next Steps

  1. โœ… Example 06 - External Services (proxy.Service with wrappers)
  2. โœ… Example 07 - Remote Router (You are here)

๐Ÿ’ก Key Takeaway: Use proxy.Router for quick, simple API integrations. Upgrade to proxy.Service when you need structure, typing, and reusability. Choose based on complexity, not cargo-culting!