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
- โ
Using
proxy.Routerfor direct HTTP calls - โ
No service wrapper needed (simpler than
proxy.Service) - โ Simple URL config (no special definitions)
- โ Quick integration without convention/metadata
- โ
When to use
proxy.Routervsproxy.Service - โ Error handling with external APIs
๐๏ธ 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:
- โ No
PaymentServiceRemotewrapper - โ No
RegisterServiceTypefor remote - โ
Direct
proxy.Router.DoJSON()calls - โ Much simpler for quick integrations!
๐ 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:
- โ No separate router-definitions section
- โ URL directly in service config
- โ Easy to override per environment
- โ Clear configuration intent
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
¤t, // 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:
- โ
Direct
DoJSON()calls - โ Manual URL construction
- โ No service wrapper needed
- โ Simple and straightforward
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:
- Read URL from config
- Create router with
proxy.NewRemoteRouter(url) - No framework injection needed
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:
GetWeatherReport()โGET /weather-reports/{id}(not used in this example)- Or accessed via
POST /weather-reportswith query params
๐ฏ proxy.Router API
Available Methods
- 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 ) - Get - Simple GET requests
resp, err := router.Get("/weather/jakarta", headers) - PostJSON - POST with JSON body
resp, err := router.PostJSON("/endpoint", data, headers) - 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:
- Quick integration needed
- One-off API calls
- Prototyping/testing external APIs
- Simple endpoints (1-3 calls)
- No need for reusable service abstraction
- Example: Weather API, currency converter, IP geolocation
When to Use proxy.Service
โ USE proxy.Service when:
- Multiple related endpoints
- Need service abstraction
- Want typed methods
- Dependency injection required
- Complex business logic
- Example: Payment gateway, email service, SMS provider
๐ก 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:
- Making 5+ calls to same API
- Repeating URL construction logic
- Need to mock for testing
- Want stronger typing
- Service used by multiple parts of code
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
- โ
Example 06 - External Services (
proxy.Servicewith wrappers) - โ Example 07 - Remote Router (You are here)
๐ Related Documentation
๐ก 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!