Example 06 - External Services Integration
This example demonstrates how to integrate external APIs (like payment gateways, email services, SMS providers) as Lokstra services using proxy.Service for convention-based remote calls.
π What Youβll Learn
- β Wrapping third-party APIs as Lokstra services
- β
Using
proxy.Servicefor remote HTTP calls - β
Route overrides in
RegisterServiceType(not in config!) - β
external-service-definitionswith URL and factory type - β Business services depending on external services
- β Error handling when external service fails
- β Clean service code without metadata embedding
- β
Difference between
proxy.Servicevsproxy.Router(see Example 07)
ποΈ Architecture
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main App (:3000) β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β OrderService (Business Logic) β β
β β - Create() β POST /orders β β
β β - Get() β GET /orders/{id} β β
β β - Refund() β POST /orders/{id}/refund β β
β βββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ β
β β depends on β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β PaymentServiceRemote (proxy.Service) β β
β β - CreatePayment() β POST /payments β β
β β - GetPayment() β GET /payments/{id} β β
β β - Refund() β POST /payments/{id}/refund β β
β βββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ β
β β HTTP calls β
ββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββ
β Mock Payment Gateway (:9000) β
β (Simulates Stripe, PayPal, etc.) β
β β
β POST /payments β
β GET /payments/{id} β
β POST /payments/{id}/refund β
βββββββββββββββββββββββββββββββββββββββββββββββββ
Key: All route overrides defined in RegisterServiceType in main.go!
π How to Run
Step 1: Start Mock Payment Gateway
cd mock-payment-gateway
go run main.go
This starts the mock payment gateway on http://localhost:9000. It simulates an external payment provider like Stripe or PayPal.
Step 2: Start Main Application
# From the example root directory
go run main.go
This starts the main application on http://localhost:3000.
Step 3: Test with HTTP Requests
Use the test.http file or curl:
# Create order (processes payment via external gateway)
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{
"user_id": 1,
"items": ["Laptop", "Mouse", "Keyboard"],
"total_amount": 1299.99,
"currency": "USD"
}'
# Get order
curl http://localhost:3000/orders/order_1
# Refund order (via external gateway)
curl -X POST http://localhost:3000/orders/order_1/refund
π Project Structure
06-external-services/
βββ main.go # Main application entry point
βββ config.yaml # Configuration with external service definitions
βββ test.http # HTTP test scenarios
βββ index # This file
β
βββ mock-payment-gateway/
β βββ main.go # Mock external payment API
β
βββ service/
βββ payment_service_remote.go # Proxy to external payment gateway
βββ order_service.go # Business logic using external payment
π Key Concepts
1. External Service Definition
Define external services in config.yaml with URL and factory type:
external-service-definitions:
payment-gateway:
url: "http://localhost:9000"
type: payment-service-remote-factory
What it does:
- Declares external API location
- Specifies factory type for creating wrapper
- Framework creates proxy.Service automatically with this URL
2. Remote Service Wrapper
Create a clean service wrapper without embedded metadata:
// PaymentServiceRemote wraps external payment API
type PaymentServiceRemote struct {
proxyService *proxy.Service
}
func NewPaymentServiceRemote(proxyService *proxy.Service) *PaymentServiceRemote {
return &PaymentServiceRemote{
proxyService: proxyService,
}
}
// Method names can be non-standard (routes defined in RegisterServiceType)
func (s *PaymentServiceRemote) CreatePayment(p *CreatePaymentParams) (*Payment, error) {
return proxy.CallWithData[*Payment](s.proxyService, "CreatePayment", p)
}
func (s *PaymentServiceRemote) GetPayment(p *GetPaymentParams) (*Payment, error) {
return proxy.CallWithData[*Payment](s.proxyService, "GetPayment", p)
}
func (s *PaymentServiceRemote) Refund(p *RefundParams) (*RefundResponse, error) {
return proxy.CallWithData[*RefundResponse](s.proxyService, "Refund", p)
}
Key points:
- β
Simple struct with
proxyServicefield - β NO metadata interfaces (no ServiceMeta!)
- β Method names can be non-standard (CreatePayment, GetPayment)
- β
Routes defined separately in
RegisterServiceType
3. Service Registration with Metadata
Register in main.go with all metadata and route overrides:
// Register remote-only service (nil local factory)
lokstra_registry.RegisterServiceType(
"payment-service-remote-factory",
nil, // No local implementation
svc.PaymentServiceRemoteFactory, // Remote factory
deploy.WithResource("payment", "payments"),
deploy.WithConvention("rest"),
// Route overrides for non-standard method names
deploy.WithRouteOverride("CreatePayment", "POST /payments"),
deploy.WithRouteOverride("GetPayment", "GET /payments/{id}"),
deploy.WithRouteOverride("Refund", "POST /payments/{id}/refund"),
)
// Register local business service with custom action
lokstra_registry.RegisterServiceType(
"order-service-factory",
svc.OrderServiceFactory, nil,
deploy.WithResource("order", "orders"),
deploy.WithConvention("rest"),
// Custom action route
deploy.WithRouteOverride("Refund", "POST /orders/{id}/refund"),
)
Why route overrides?
CreatePayment,GetPaymentβ standard REST names (Create,Get)Refundis custom action (not standard REST)- Allows matching external API exactly as-is
4. Remote Factory Implementation
Framework injects proxy.Service via config["remote"]:
func PaymentServiceRemoteFactory(deps map[string]any, config map[string]any) any {
return NewPaymentServiceRemote(
service.CastProxyService(config["remote"]),
)
}
What happens:
- Framework reads
external-service-definitions.payment-gateway.url - Creates
proxy.Servicewith URL ="http://localhost:9000" - Passes it via
config["remote"]to factory - Factory wraps it in
PaymentServiceRemote
5. Business Service Using External Service
Clean service code with standard REST method names:
type OrderService struct {
Payment *service.Cached[*PaymentServiceRemote]
}
func OrderServiceFactory(deps map[string]any, config map[string]any) any {
return &OrderService{
Payment: service.Cast[*PaymentServiceRemote](deps["payment-gateway"]),
}
}
// Standard REST method names (Create, Get, not CreateOrder, GetOrder)
func (s *OrderService) Create(p *OrderCreateParams) (*Order, error) {
// Create order
order := &Order{
ID: fmt.Sprintf("order_%d", orderID),
Status: "pending",
...
}
// Process payment via external gateway
payment, err := s.Payment.MustGet().CreatePayment(&CreatePaymentParams{
Amount: p.TotalAmount,
Currency: p.Currency,
Description: fmt.Sprintf("Payment for order %s", order.ID),
})
if err != nil {
order.Status = "failed"
return nil, fmt.Errorf("payment failed: %w", err)
}
order.PaymentID = payment.ID
order.Status = "paid"
return order, nil
}
func (s *OrderService) Get(p *OrderGetParams) (*Order, error) {
// Retrieve order by ID
}
func (s *OrderService) Refund(p *OrderRefundParams) (*Order, error) {
// Process refund via external gateway
_, err := s.Payment.MustGet().Refund(&RefundParams{
ID: order.PaymentID,
})
if err != nil {
return nil, fmt.Errorf("refund failed: %w", err)
}
order.Status = "refunded"
return order, nil
}
Key points:
- β Clean service struct (no metadata interfaces!)
- β
Standard REST method names:
Create,Get,Refund - β
Only
Refundneeds route override (custom action) - β
Depends on external service via
deps["payment-gateway"]
π― Service Configuration
In config.yaml:
# Define external API
external-service-definitions:
payment-gateway:
url: "http://localhost:9000"
type: payment-service-remote-factory
# Define local business service
service-definitions:
order-service:
type: order-service-factory
depends-on:
- payment-gateway # Reference external service
deployments:
app:
servers:
api-server:
base-url: "http://localhost"
addr: ":3000"
published-services:
- order-service
# Framework auto-detects payment-gateway dependency
How it works:
- Framework reads
order-servicedependencies - Finds
payment-gatewayinexternal-service-definitions - Creates
proxy.Servicewith URL from config - Calls
PaymentServiceRemoteFactorywith proxy - Injects into
OrderServiceviadeps["payment-gateway"]
π Request Flow
- Client β
POST /ordersto main app (:3000) - OrderService β Validate request, create order
- OrderService β Call
Payment.MustGet().CreatePayment() - PaymentServiceRemote β HTTP call to
:9000/payments - Mock Gateway β Process payment, return payment ID
- OrderService β Update order with payment ID, status = βpaidβ
- Client β Return order with payment details
π Comparison: proxy.Service vs proxy.Router
| Feature | proxy.Service (This Example) | proxy.Router (Example 07) |
|---|---|---|
| Use Case | Structured external services | Quick API access |
| Convention | β REST/JSON-RPC auto-routing | β Manual paths |
| Type Safety | β Typed methods | β Generic calls |
| Overrides | β Custom route overrides | N/A |
| Service Wrapper | β Required | β Not needed |
| Best For | Payment, Email, SMS APIs | Weather, Maps, Ad-hoc APIs |
When to use proxy.Service:
- External API has multiple related endpoints
- You want typed methods and reusability
- Need service dependency injection
- Example: Stripe, SendGrid, Twilio
When to use proxy.Router:
- One-off API calls
- Quick integration without wrapper
- No need for service abstraction
- Example: Weather API, Currency converter
π§ͺ Mock Payment Gateway
The mock gateway simulates a real payment provider using Lokstra framework:
package main
import (
"fmt"
"log"
"sync"
"time"
"github.com/primadi/lokstra"
)
// In-memory storage
var (
payments = make(map[string]*Payment)
paymentsMu sync.RWMutex
nextID = 1
)
// Handlers using Lokstra's handler form variations
func createPayment(req *CreatePaymentRequest) (*Payment, error) {
if req.Currency == "" {
req.Currency = "USD"
}
paymentsMu.Lock()
id := fmt.Sprintf("pay_%d", nextID)
nextID++
payment := &Payment{
ID: id,
Amount: req.Amount,
Currency: req.Currency,
Status: "completed",
Description: req.Description,
CreatedAt: time.Now(),
}
payments[id] = payment
paymentsMu.Unlock()
log.Printf("β
Payment created: %s - $%.2f %s", id, req.Amount, req.Currency)
return payment, nil
}
func getPayment(req *GetPaymentRequest) (*Payment, error) {
paymentsMu.RLock()
payment, exists := payments[req.ID]
paymentsMu.RUnlock()
if !exists {
return nil, fmt.Errorf("payment not found: %s", req.ID)
}
return payment, nil
}
func refundPayment(req *RefundRequest) (*RefundResponse, error) {
paymentsMu.Lock()
defer paymentsMu.Unlock()
payment, exists := payments[req.ID]
if !exists {
return nil, fmt.Errorf("payment not found: %s", req.ID)
}
if payment.Status != "completed" {
return nil, fmt.Errorf("only completed payments can be refunded")
}
now := time.Now()
payment.Status = "refunded"
payment.RefundedAt = &now
log.Printf("πΈ Payment refunded: %s", req.ID)
return &RefundResponse{
PaymentID: req.ID,
RefundedAt: now,
Status: "refunded",
Message: fmt.Sprintf("Payment %s has been refunded", req.ID),
}, nil
}
func main() {
// Create router with Lokstra
r := lokstra.NewRouter("payment-api")
// Register routes
r.POST("/payments", createPayment)
r.GET("/payments/{id}", getPayment)
r.POST("/payments/{id}/refund", refundPayment)
// Start server
app := lokstra.NewApp("payment-gateway", ":9000", r)
if err := app.Run(30 * time.Second); err != nil {
log.Fatalf("Failed to run app: %v", err)
}
}
Key points:
- β Built with Lokstra (not standard http package)
- β Demonstrates Lokstraβs handler form flexibility
- β Uses struct parameters with validation tags
- β In-memory storage with sync.RWMutex
- β Instant success (status = βcompletedβ)
- β Simple refund logic
Endpoints:
POST /payments- Create paymentGET /payments/{id}- Get payment statusPOST /payments/{id}/refund- Refund payment
π Learning Points
1. External Service Integration Pattern
External API β proxy.Service β Service Wrapper β Business Service
This pattern:
- Isolates external API details
- Provides typed interface
- Enables testing with mocks
- Centralizes error handling
2. Route Overrides for Non-Standard APIs
Use deploy.WithRouteOverride() when:
- Method names donβt match REST (
CreatePaymentvsCreate) - Custom actions needed (
POST /orders/{id}/refund) - External API has specific requirements
Standard REST methods (no override needed):
Create()βPOST /resourceGet()βGET /resource/{id}Update()βPUT /resource/{id}Delete()βDELETE /resource/{id}List()βGET /resource
Non-standard (override required):
CreatePayment()β needsPOST /paymentsRefund()β needsPOST /payments/{id}/refund
3. Clean Separation of Concerns
- Service code: Pure logic, no metadata
- Registration: Metadata + route overrides in
main.go - Config: Deployment topology only
This makes services:
- Easier to test (no framework coupling)
- Simpler to understand (one responsibility)
- More maintainable (metadata in one place)
4. Error Handling
When external service fails:
payment, err := s.Payment.MustGet().CreatePayment(...)
if err != nil {
order.Status = "failed"
return nil, fmt.Errorf("payment failed: %w", err)
}
Always handle external failures gracefully and update your domain state accordingly!
π Next Steps
- β Example 06 - External Services (You are here)
- π Example 07 - Remote Router (
proxy.Routerfor quick API access)
π― Real-World Examples
This pattern works for any external API:
Payment Gateways:
- Stripe:
stripe-service-remoteβhttps://api.stripe.com - PayPal:
paypal-service-remoteβhttps://api.paypal.com
Communication:
- SendGrid:
email-service-remoteβhttps://api.sendgrid.com - Twilio:
sms-service-remoteβhttps://api.twilio.com
Storage:
- AWS S3:
s3-service-remoteβhttps://s3.amazonaws.com - Cloudinary:
cdn-service-remoteβhttps://api.cloudinary.com
All follow the same pattern: define external service β create wrapper β use in business services!
π Related Documentation
- Architecture - Service Categories
- Architecture - Proxy Patterns
- Remote Services Guide
- Configuration Guide
π‘ Key Takeaway: Use proxy.Service to wrap external APIs as typed Lokstra services with convention-based routing and custom overrides. For simpler one-off calls, use proxy.Router (Example 07).