Example 4: Multi-Deployment with Auto-Router & Proxy

Demonstrates: Convention-based auto-router generation, metadata-driven routing, and seamless local/remote service switching


πŸ“Œ About This Example

This example showcases Lokstra’s production-ready patterns for building flexible, deployment-agnostic applications:

Key Features:

What’s New vs Manual Approach:


🎯 Learning Objectives

  1. Convention-Based Routing: How services auto-generate RESTful endpoints
  2. Clean Architecture: Separation of concerns with contract, model, service, repository layers
  3. Metadata Architecture: Single source of truth in RegisterServiceType
  4. 2-Level Metadata System: RegisterServiceType β†’ YAML config overrides
  5. Auto-Proxy Pattern: proxy.CallWithData with convention mapping
  6. Deployment Flexibility: Same code, different runtime behavior
  7. Interface-Based DI: Depend on contracts, not implementations

πŸ” Framework Comparison

Want to see how this same application would be implemented in other frameworks?

πŸ‘‰ Framework Comparison: Lokstra vs NestJS vs Spring Boot - Same functionality, different approaches with detailed code examples and trade-offs analysis.


πŸ—οΈ Architecture

Deployment 1: Monolith (1 Server)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   API Server (Port 3003)               β”‚
β”‚                                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  UserServiceImpl (local)         β”‚  β”‚
β”‚  β”‚  Auto-Router:                    β”‚  β”‚
β”‚  β”‚  β€’ GET /users                    β”‚  β”‚
β”‚  β”‚  β€’ GET /users/{id}               β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                ↑                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  OrderServiceImpl (local)        β”‚  β”‚
β”‚  β”‚  Auto-Router:                    β”‚  β”‚
β”‚  β”‚  β€’ GET /orders/{id}              β”‚  β”‚
β”‚  β”‚  β€’ GET /users/{user_id}/orders   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                        β”‚
β”‚  Direct method calls (in-process)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Deployment 2: Microservices (2 Servers)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  User Server          β”‚         β”‚  Order Server               β”‚
β”‚  (Port 3004)          β”‚         β”‚  (Port 3005)                β”‚
β”‚                       β”‚         β”‚                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  HTTP   β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ UserServiceImpl β”‚  │◄─────────  β”‚ UserServiceRemote     β”‚  β”‚
β”‚  β”‚ Auto-Router:    β”‚  β”‚         β”‚  β”‚ (proxy.Service)       β”‚  β”‚
β”‚  β”‚ β€’ GET /users    β”‚  β”‚         β”‚  β”‚ β€’ CallWithData        β”‚  β”‚
β”‚  β”‚ β€’ GET /users/{id}  β”‚         β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚         β”‚            ↑                β”‚
β”‚                       β”‚         β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚  β”‚ OrderServiceImpl      β”‚  β”‚
                                  β”‚  β”‚ Auto-Router:          β”‚  β”‚
                                  β”‚  β”‚ β€’ GET /orders/{id}    β”‚  β”‚
                                  β”‚  β”‚ β€’ GET /users/{uid}/   β”‚  β”‚
                                  β”‚  β”‚   orders              β”‚  β”‚
                                  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key: UserServiceRemote uses metadata to auto-map methods to HTTP endpoints

πŸ“¦ Project Structure

04-multi-deployment/
β”œβ”€β”€ contract/                    # Application Layer - Interfaces & DTOs
β”‚   β”œβ”€β”€ user_contract.go         # UserService interface + request/response types
β”‚   └── order_contract.go        # OrderService interface + DTOs
β”‚
β”œβ”€β”€ model/                       # Domain Layer - Pure business entities
β”‚   β”œβ”€β”€ user.go                  # User entity
β”‚   └── order.go                 # Order entity
β”‚
β”œβ”€β”€ repository/                  # Infrastructure Layer - Data access
β”‚   β”œβ”€β”€ user_repository.go       # UserRepository interface + in-memory impl
β”‚   └── order_repository.go      # OrderRepository interface + in-memory impl
β”‚
β”œβ”€β”€ service/                     # Application Layer - Business logic
β”‚   β”œβ”€β”€ user_service.go          # UserServiceImpl (local implementation)
β”‚   β”œβ”€β”€ user_service_remote.go   # UserServiceRemote (HTTP proxy)
β”‚   β”œβ”€β”€ order_service.go         # OrderServiceImpl (local implementation)
β”‚   └── order_service_remote.go  # OrderServiceRemote (HTTP proxy)
β”‚
β”œβ”€β”€ config.yaml                  # Multi-deployment configuration
β”œβ”€β”€ main.go                      # Entry point with service registration
└── test.http                    # API tests

πŸ›οΈ Clean Architecture Layers

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Application Layer (service/, contract/)                β”‚
β”‚  - Business logic & use cases                           β”‚
β”‚  - Service interfaces & DTOs                            β”‚
β”‚  - Depends on: Domain Layer                             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Domain Layer (model/)                                  β”‚
β”‚  - Pure business entities                               β”‚
β”‚  - No external dependencies                             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Infrastructure Layer (repository/)                     β”‚
β”‚  - Data access implementations                          β”‚
β”‚  - External service adapters                            β”‚
β”‚  - Depends on: Domain interfaces                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Dependency Rule: Outer layers depend on inner layers, never the reverse

πŸ”‘ Key Concepts

1. Clean Architecture Pattern

This example follows Clean Architecture principles with clear separation of concerns:

Layer 1: Domain (model/)

Pure business entities with no external dependencies:

// model/user.go
package model

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

Characteristics:

Layer 2: Application (contract/, service/)

contract/user_contract.go - Service interfaces & DTOs:

package contract

import "example/model"

// Service interface (application boundary)
type UserService interface {
    GetByID(p *GetUserParams) (*model.User, error)
    List(p *ListUsersParams) ([]*model.User, error)
}

// DTOs (Data Transfer Objects)
type GetUserParams struct {
    ID int `path:"id"`
}

service/user_service.go - Business logic implementation:

package service

import "example/contract"
import "example/repository"

type UserServiceImpl struct {
    UserRepo *service.Cached[repository.UserRepository]  // Depend on interface!
}

var _ contract.UserService = (*UserServiceImpl)(nil)  // Ensure implementation

func (s *UserServiceImpl) GetByID(p *contract.GetUserParams) (*model.User, error) {
    return s.UserRepo.MustGet().GetByID(p.ID)
}

Characteristics:

Layer 3: Infrastructure (repository/)

repository/user_repository.go - Data access:

package repository

import "example/model"

// Repository interface (defined by application layer needs)
type UserRepository interface {
    GetByID(id int) (*model.User, error)
    List() ([]*model.User, error)
}

// Implementation (infrastructure detail)
type UserRepositoryMemory struct {
    users map[int]*model.User
}

var _ UserRepository = (*UserRepositoryMemory)(nil)

func (r *UserRepositoryMemory) GetByID(id int) (*model.User, error) {
    // Implementation details
}

Characteristics:

Dependency Flow

main.go β†’ service β†’ repository interface
                 ↓
              model

Dependencies point INWARD (toward domain)

Benefits:

  1. Testability: Easy to mock repositories and test services
  2. Flexibility: Swap implementations without changing business logic
  3. Maintainability: Clear boundaries between layers
  4. Scalability: Add features without touching existing code
  5. Team collaboration: Clear ownership per layer

2. Metadata via RegisterServiceType

Services provide routing metadata via RegisterServiceType options:

main.go:

lokstra_registry.RegisterServiceType("user-service-factory",
    service.UserServiceFactory,
    service.UserServiceRemoteFactory,
    deploy.WithResource("user", "users"),
    deploy.WithConvention("rest"),
)

lokstra_registry.RegisterServiceType("order-service-factory",
    service.OrderServiceFactory,
    service.OrderServiceRemoteFactory,
    deploy.WithResource("order", "orders"),
    deploy.WithConvention("rest"),
    deploy.WithRouteOverride("GetByUserID", "GET /users/{user_id}/orders"),
)

Benefits:

3. Auto-Router Generation

Framework scans service methods and generates routes using conventions:

// Service method
func (s *UserServiceImpl) GetByID(p *GetUserParams) (*User, error)

// Auto-generates
GET /users/{id} -> UserService.GetByID

Convention mapping: | Method Name | HTTP Method | Path | |β€”β€”β€”β€”-|β€”β€”β€”β€”-|β€”β€”| | List() | GET | /users | | GetByID(params) | GET | /users/{id} | | Create(params) | POST | /users | | Update(params) | PUT | /users/{id} | | Delete(params) | DELETE | /users/{id} | | Custom actions | POST | /actions/{snake_case} |

4. Custom Route Overrides

Override convention-based routes via RegisterServiceType:

main.go:

lokstra_registry.RegisterServiceType("order-service-factory",
    service.OrderServiceFactory,
    service.OrderServiceRemoteFactory,
    deploy.WithResource("order", "orders"),
    deploy.WithConvention("rest"),
    deploy.WithRouteOverride("GetByUserID", "GET /users/{user_id}/orders"),
)

Format: "METHOD /path" or just "/path" (defaults to GET)

Result:

5. Proxy.Service Pattern

Remote services use proxy.CallWithData for type-safe HTTP calls:

func (u *UserServiceRemote) GetByID(params *GetUserParams) (*User, error) {
    return proxy.CallWithData[*User](u.GetProxyService(), "GetByID", params)
}

What happens:

  1. Framework resolves method name to HTTP route using metadata
  2. Extracts path params from struct tags (path:"id")
  3. Makes HTTP request
  4. Auto-extracts data from JSON wrapper ({"data": {...}})
  5. Returns typed result

No manual URL construction, no manual JSON parsing!

6. 2-Level Metadata System

Metadata can be provided in 2 places with priority:

Priority 1 (HIGH):  YAML config (router-overrides)     ← Deployment-specific overrides
Priority 2 (MED):   RegisterServiceType options        ← Default metadata
Priority 3 (LOW):   Auto-generate from service name    ← Fallback

Recommended: Put metadata in RegisterServiceType options. Use YAML only for deployment-specific overrides.


πŸš€ Running the Examples

Option 1: Monolith Deployment

go run . -server "monolith.api-server"

Output:

Starting [api-server] with 2 router(s) on address :3003
[user-auto] GET /users/{id} -> user-auto.GetByID
[user-auto] GET /users -> user-auto.List
[order-auto] GET /orders/{id} -> order-auto.GetByID
[order-auto] GET /users/{user_id}/orders -> order-auto.GetByUserID

All endpoints on port 3003.

Option 2: Microservices Deployment

Terminal 1 - User Server:

go run . -server "microservice.user-server"

Output:

Starting [user-server] with 1 router(s) on address :3004
[user-auto] GET /users/{id} -> user-auto.GetByID
[user-auto] GET /users -> user-auto.List

Terminal 2 - Order Server:

go run . -server "microservice.order-server"

Output:

Starting [order-server] with 1 router(s) on address :3005
[order-auto] GET /orders/{id} -> order-auto.GetByID
[order-auto] GET /users/{user_id}/orders -> order-auto.GetByUserID

πŸ“ Configuration Walkthrough

config.yaml

service-definitions:
  # Infrastructure layer - Repositories
  user-repository:
    type: user-repository-factory

  order-repository:
    type: order-repository-factory

  # Application layer - Services
  user-service:
    type: user-service-factory
    depends-on: [user-repository]
  
  order-service:
    type: order-service-factory
    depends-on: [order-repository, user-service]  # Can be local OR remote

# Routers auto-generated from published-services using metadata
# Optional overrides commented out - metadata in XXXRemote is enough!

deployments:
  monolith:
    servers:
      api-server:
        base-url: "http://localhost"
        addr: ":3003"
        published-services:
          - user-service
          - order-service

  microservice:
    servers:
      user-server:
        base-url: "http://localhost"
        addr: ":3004"
        published-services: [user-service]

      order-server:
        base-url: "http://localhost"
        addr: ":3005"
        published-services: [order-service]
        # Framework auto-detects user-service-remote from topology

Key Points:

main.go

func main() {
    // Register repositories (infrastructure layer)
    lokstra_registry.RegisterServiceType("user-repository-factory",
        repository.NewUserRepositoryMemory, nil)

    lokstra_registry.RegisterServiceType("order-repository-factory",
        repository.NewOrderRepositoryMemory, nil)

    // Register services (application layer) with metadata
    lokstra_registry.RegisterServiceType("user-service-factory",
        service.UserServiceFactory,
        service.UserServiceRemoteFactory,
        deploy.WithResource("user", "users"),
        deploy.WithConvention("rest"),
    )

    lokstra_registry.RegisterServiceType("order-service-factory",
        service.OrderServiceFactory,
        service.OrderServiceRemoteFactory,
        deploy.WithResource("order", "orders"),
        deploy.WithConvention("rest"),
        deploy.WithRouteOverride("GetByUserID", "GET /users/{user_id}/orders"),
    )

    // Load config - auto-builds ALL deployments
    lokstra_registry.LoadAndBuild([]string{"config.yaml"})

    // Run server based on flag
    lokstra_registry.RunServer(*server, 30*time.Second)
}

What’s simplified:


πŸ§ͺ Testing

Use test.http with VS Code REST Client extension:

### Monolith - Get all users
GET http://localhost:3003/users

### Monolith - Get specific user
GET http://localhost:3003/users/1

### Monolith - Get order with user (cross-service)
GET http://localhost:3003/orders/1

### Monolith - Get user's orders
GET http://localhost:3003/users/1/orders

### Microservices - User server
GET http://localhost:3004/users/1

### Microservices - Order server (makes HTTP call to user server)
GET http://localhost:3005/orders/1
GET http://localhost:3005/users/1/orders

πŸ” How It Works

Auto-Router Generation Flow

1. Config loading (LoadAndBuild):
   β”œβ”€ Read config.yaml
   β”œβ”€ Find published-services: [user-service, order-service]
   β”œβ”€ Create router definitions: [user-service-router, order-service-router]
   └─ Register to global registry

2. Server startup (RunServer):
   β”œβ”€ Get router definitions for current server
   β”œβ”€ Call BuildRouterFromDefinition for each:
   β”‚  β”œβ”€ Get service from registry (local or remote auto-resolved)
   β”‚  β”œβ”€ Read metadata from RegisterServiceType options
   β”‚  β”œβ”€ Merge YAML overrides if any
   β”‚  β”œβ”€ Call autogen.NewFromService(svc, rule, override)
   β”‚  β”‚  β”œβ”€ Scan service methods via reflection
   β”‚  β”‚  β”œβ”€ Map methods to routes using convention
   β”‚  β”‚  β”œβ”€ Apply custom overrides from metadata
   β”‚  β”‚  └─ Create auto-generated handlers
   β”‚  └─ Return router
   └─ Mount all routers to app

3. Request handling:
   β”œβ”€ HTTP request arrives
   β”œβ”€ Router matches path to auto-generated handler
   β”œβ”€ Handler calls service method
   β”œβ”€ Service method:
   β”‚  β”œβ”€ Monolith: Direct method call (UserServiceImpl)
   β”‚  └─ Microservices: HTTP proxy call (UserServiceRemote)
   β”‚     └─ proxy.CallWithData makes HTTP request to remote server
   └─ Return response

Cross-Service Call Flow (Microservices)

Client β†’ Order Server β†’ User Server
                ↓
GET /orders/1   OrderServiceImpl.GetByID()
                ↓
                s.Users.MustGet().GetByID(...)
                ↓
                UserServiceRemote.GetByID()
                ↓
                proxy.CallWithData[*User](service, "GetByID", params)
                ↓
                [Metadata resolution]
                Resource: "user", Plural: "users", Convention: "rest"
                Method: "GetByID" β†’ Convention: GET /users/{id}
                ↓
                HTTP GET http://localhost:3004/users/1
                ↓
                UserServiceImpl.GetByID() @ User Server
                ↓
                Return User

πŸ’‘ Design Patterns

1. Convention Over Configuration

Instead of:

# ❌ Manual route definitions
routers:
  user-router:
    routes:
      - path: /users
        method: GET
        handler: listUsers
      - path: /users/{id}
        method: GET
        handler: getUser

We have:

// βœ… Convention-based auto-generation
type UserService interface {
    List()     // β†’ GET /users
    GetByID()  // β†’ GET /users/{id}
}

2. Metadata-Driven Architecture

Single source of truth in service registration:

lokstra_registry.RegisterServiceType("order-service-factory",
    service.OrderServiceFactory,
    service.OrderServiceRemoteFactory,
    deploy.WithResource("order", "orders"),
    deploy.WithConvention("rest"),
    deploy.WithRouteOverride("GetByUserID", "GET /users/{user_id}/orders"),
)

Used by:

Benefits:

3. Interface-Based Dependency Injection

type OrderServiceImpl struct {
    Users *service.Cached[UserService]  // Interface!
}

Runtime resolution:

Same code, different behavior!

4. Zero-Boilerplate Remote Calls

Before (manual):

var wrapper struct {
    Data *User `json:"data"`
}
err := proxy.DoJSON("GET", fmt.Sprintf("/users/%d", id), nil, nil, &wrapper)
return wrapper.Data, err

After (auto):

return proxy.CallWithData[*User](service, "GetByID", params)

Framework handles:


πŸŽ“ Advanced Topics

Custom Conventions

Create your own routing conventions:

convention.Register("api-v2", &convention.Definition{
    List:     "GET /{resource}",
    GetByID:  "GET /{resource}/{id}",
    Create:   "POST /{resource}",
    // ... custom patterns
})

Deployment-Specific Overrides

Override metadata per environment:

# production.yaml
routers:
  order-service-router:
    overrides: prod-overrides

router-overrides:
  prod-overrides:
    path-prefix: /api/v2  # All routes prefixed
    hidden: [InternalMethod]  # Hide from public

Service Discovery Integration

Auto-resolve service URLs:

external-service-definitions:
  user-service:
    url: "http://user-service.default.svc.cluster.local"
    type: user-service-remote-factory
    
deployments:
  kubernetes:
    servers:
      order-server:
        published-services: [order-service]
        # user-service-remote auto-detected from external-service-definitions

πŸ“Š Comparison: Manual vs Auto

Aspect Manual Approach Auto (This Example)
Router Creation Manual r.GET() Auto-generated
Handler Code Manual functions Auto-generated
Proxy Calls Manual DoJSON() CallWithData()
Route Metadata Hardcoded strings Convention + metadata
Custom Routes Manual registration Override in metadata
Lines of Code ~200 lines ~40 lines
Refactoring Manual updates Auto-updates

Code reduction: 80% less boilerplate!


πŸš€ Production Considerations

1. Service URLs

Development (current):

ProxyService: proxyService  // Framework-injected

Production:

external-service-definitions:
  user-service-remote:
    url: "http://user-service.prod.internal:3004"
    timeout: 5s

2. Error Handling

Add circuit breakers, retries:

func (s *OrderServiceImpl) GetByID(p *GetOrderParams) (*OrderWithUser, error) {
    user, err := s.Users.MustGet().GetByID(&GetUserParams{ID: order.UserID})
    if err != nil {
        // Handle remote call failure
        if apiErr, ok := err.(*api_client.ApiError); ok {
            return nil, fmt.Errorf("user service error: %s", apiErr.Message)
        }
        return nil, err
    }
    // ...
}

3. Monitoring

Framework logs auto-router generation:

✨ Auto-generated router 'user-service-router' from service 'user-service'
✨ Auto-generated router 'order-service-router' from service 'order-service'

Add custom metrics:

proxy.CallWithData[*User](service, "GetByID", params)
// Framework can track: latency, errors, retries

🎯 Key Takeaways

Why This Approach Is Better

  1. Less Code: 80% reduction vs manual approach
  2. Type Safety: Compile-time checks for method mapping
  3. Single Source of Truth: Metadata in service code
  4. Refactoring-Friendly: Rename method β†’ route auto-updates
  5. Convention-Based: Follow standards (REST, JSON:API, etc.)
  6. Flexible: Override when needed via metadata or YAML

When to Use Auto-Router

βœ… Good for:

❌ Consider manual when:

Production Checklist

Before deploying:



πŸ’‘ What You Learned

  1. βœ… Clean Architecture with contract, model, service, repository layers
  2. βœ… Auto-router generation from service methods
  3. βœ… RemoteServiceMeta interface for metadata
  4. βœ… 3-level metadata system (code β†’ options β†’ YAML)
  5. βœ… proxy.CallWithData for type-safe HTTP calls
  6. βœ… Convention-based routing (REST, custom)
  7. βœ… Single binary, multiple deployments
  8. βœ… Zero-boilerplate remote service calls
  9. βœ… Metadata-driven architecture
  10. βœ… Interface-based dependency injection for testability

Next: Explore custom conventions and advanced routing patterns! πŸš€