Architecture
Understanding Lokstraβs design - how all the pieces fit together
π Note: All code examples in this document are runnable - you can copy-paste and execute them directly. Examples are using the actual Lokstra API, not simplified pseudocode.
π― Overview
Lokstra is built on 6 core components that work together to create a flexible, scalable REST API framework:
ββββββββββββββββββββββββββββββββββββββββββββββββ
β SERVER β
β (Container - Lifecycle Management) β
β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β APP β β
β β (HTTP Listener - Standard net/http) β β
β β β β
β β ββββββββββββββββββββββββββββββββββββββ β β
β β β ROUTER β β β
β β β (Route Management + Middleware) β β β
β β β β β β
β β β Route 1 β [MW1, MW2] β Handler β β β
β β β Route 2 β [MW3] β Handler β β β
β β β Route 3 β Handler β Service β β β
β β ββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββ
Supporting Components:
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β SERVICE β β MIDDLEWARE β βCONFIGURATIONβ
β (Business) β β (Filters) β β (Settings) β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
Letβs explore each component:
ποΈ Component 1: Server
Purpose: Container for one or more Apps, manages lifecycle
Responsibilities
- β Start/stop Apps
- β Graceful shutdown
- β Signal handling (SIGTERM, SIGINT)
- β Configuration loading
Key Point: Not in Request Flow!
β WRONG: Request β Server β App β Router
β
RIGHT: Request β App β Router
β
Server manages lifecycle only
Example Usage
import "github.com/primadi/lokstra"
func main() {
// Create routers
apiV1Router := lokstra.NewRouter("api-v1")
apiV2Router := lokstra.NewRouter("api-v2")
adminRouter := lokstra.NewRouter("admin")
setupApiV1Router(apiV1Router)
setupApiV2Router(apiV2Router)
setupAdminRouter(adminRouter)
// Create server with multiple apps
server := lokstra.NewServer("my-server",
lokstra.NewApp("api-v1", ":8080", apiV1Router),
lokstra.NewApp("api-v2", ":8081", apiV2Router),
lokstra.NewApp("admin", ":9000", adminRouter),
)
// Run with graceful shutdown (30s timeout)
server.Run(30 * time.Second)
}
When server receives SIGTERM/SIGINT:
- Stop accepting new connections
- Wait for active requests (max 30s)
- Close all apps
- Exit
π Learn more: App & Server Guide
π Component 2: App
Purpose: HTTP listener that serves a Router
Responsibilities
- β
Listen on address (
:8080) - β Accept HTTP connections
- β Pass requests to Router
- β
Implement
http.Handleror FastHTTP handler
Two Engine Types
Engine 1: Go Standard (ServeMux) - Default
// Default engine (ServeMux)
app := lokstra.NewApp("api", ":8080", router)
Engine 2: Custom Listener (Advanced)
// Custom configuration
app := lokstra.NewAppWithConfig("api", ":8080", "fasthttp",
map[string]any{
// custom config options
},
router,
)
Note: Lokstra currently uses Goβs standard net/http with ServeMux. FastHTTP support can be added via custom listener configuration if needed.
Example
import "github.com/primadi/lokstra"
func main() {
// Create router
router := lokstra.NewRouter("api")
router.GET("/ping", func() string { return "pong" })
// Create app
app := lokstra.NewApp("api", ":8080", router)
// Create server and run
server := lokstra.NewServer("my-server", app)
server.Run(30 * time.Second)
}
Request Flow Through App:
TCP Connection β App.ServeHTTP() β Router.ServeHTTP()
β
Matching Route
β
Middleware Chain
β
Handler
π Learn more: App & Server Guide
π¦ Component 3: Router
Purpose: Route registration, middleware management, request dispatch
Responsibilities
- β
Register routes (
GET,POST, etc.) - β Match incoming requests to routes
- β Apply middleware chains
- β Execute handlers
- β Support route groups
Key Features
1. Route Registration
r := lokstra.NewRouter("api")
// Simple routes
r.GET("/users", getUsers)
r.POST("/users", createUser)
// With path parameters
r.GET("/users/{id}", getUser)
// Route groups
api := r.Group("/api/v1")
api.GET("/products", getProducts) // /api/v1/products
2. Middleware Scopes
r := lokstra.NewRouter("api")
// Global middleware (all routes)
r.Use(loggingMiddleware, corsMiddleware)
// Group middleware
auth := r.Group("/admin")
auth.Use(authMiddleware)
auth.GET("/users", getUsers) // Has logging + cors + auth
auth.GET("/settings", getSettings) // Has logging + cors + auth
// Route-level middleware (single route only)
r.GET("/special", specialHandler, rateLimitMiddleware, cacheMiddleware)
// Has: logging + cors + rateLimit + cache
// No middleware (only global)
r.GET("/public", publicHandler) // Only has logging + cors
Middleware by name:
// Register middleware in registry
lokstra_registry.RegisterMiddleware("auth", authMiddleware)
lokstra_registry.RegisterMiddleware("cors", corsMiddleware)
lokstra_registry.RegisterMiddleware("logging", loggingMiddleware)
// Use by name (string reference)
r := lokstra.NewRouter("api")
r.Use("logging", "cors")
auth := r.Group("/admin")
auth.Use("auth")
auth.GET("/users", getUsers)
// Mix direct function and by-name
r.GET("/special", specialHandler, "rateLimit", cacheMiddleware)
3. Implements http.Handler
Conceptual Overview (simplified for understanding):
type Router struct {
routes []*Route
middleware []request.HandlerFunc
}
// Standard http.Handler interface
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Conceptually:
// 1. Match route based on method and path
// 2. Build middleware chain (global + route-specific)
// 3. Create request context
// 4. Execute chain with handler
// Actual implementation uses optimized routing and handler adaptation
}
Note: This is pseudo-code for conceptual understanding. The actual implementation in core/router/router_impl.go is optimized and handles:
- Path parameter extraction
- Handler form adaptation (29 variations)
- Middleware chain building
- Context creation and execution
- Error handling and response writing
See core/router/router_impl.go for real implementation details.
Routing Algorithm
Request: GET /api/users/123
Step 1: Match method β GET routes only
Step 2: Match path pattern β /api/products/{id} β /api/users/{id}
Step 3: Extract params id = β123β
Step 4: Build context ctx.PathParams[βidβ] = β123β
Step 5: Execute middleware chain [logging] β [auth] β [handler]
π Learn more: Router Guide
π§ Component 4: Service
Purpose: Business logic layer with dependency injection and service abstraction
Responsibilities
- β Implement business logic
- β Database operations
- β External API calls
- β Manage dependencies (lazy loading)
- β Support local AND remote execution
Three Service Types
Lokstra recognizes three distinct service patterns based on deployment needs:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SERVICE TYPES β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1οΈ. LOCAL ONLY (Infrastructure) β
β β’ Never exposed via HTTP β
β β’ Always loaded locally β
β β’ Examples: db, cache, logger, queue β
β β
β 2οΈ. REMOTE ONLY (External APIs) β
β β’ Third-party services β
β β’ Always accessed via HTTP β
β β’ Examples: stripe, sendgrid, twilio β
β β
β 3οΈ. LOCAL + REMOTE (Business Logic) β
β β’ Your business services β
β β’ Can be local OR remote β
β β’ Auto-published via HTTP when needed β
β β’ Examples: user-service, order-service β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Service Categories
Lokstra supports three distinct service patterns based on deployment needs:
1. Local-Only Services (Infrastructure)
Services that never need HTTP exposure:
Examples:
- Database connections (
db-service,postgres-service) - Cache systems (
redis-service,memcached-service) - Logging (
logger-service) - Message queues (
rabbitmq-service,kafka-service) - File storage (local filesystem)
Characteristics:
- β Loaded locally in process
- β Never published as router
- β No remote variant exists
- Used by other services via dependency injection
Example configuration:
service-definitions:
db-service:
type: database-factory
redis-service:
type: redis-factory
logger-service:
type: logger-factory
user-repository:
type: user-repository-factory
depends-on: [db-service]
user-service:
type: user-service-factory
depends-on: [user-repository, redis-service, logger-service]
deployments:
app:
servers:
api-server:
published-services:
- user-service
# Framework resolves dependency chain:
# user-service β [user-repository, redis-service, logger-service]
# user-repository β [db-service]
# All services created lazily when first accessed
How it works:
- Define services in
service-definitionswithdepends-on - Framework automatically resolves entire dependency chain
- Services created lazily (only when first accessed)
- No need to worry about load order - dependencies resolved on-demand
- Clean zero-config deployment!
2. Remote-Only Services (External)
Third-party APIs or external systems wrapped as Lokstra service:
Examples:
- Payment gateways (
stripe-service,paypal-service) - Email providers (
sendgrid-service,mailgun-service) - SMS services (
twilio-service) - Cloud storage (
s3-service,gcs-service) - External APIs (
weather-api-service,maps-service)
Characteristics:
- β No local implementation (always HTTP)
- β
Uses
proxy.Service(if follows convention) ORproxy.Router(custom endpoints) - β
Configured via
external-service-definitions - Can override routes for non-standard APIs
service-definitions:
order-repository:
type: order-repository-factory
depends-on: [db-service]
order-service:
type: order-service-factory
depends-on: [order-repository, payment-gateway]
external-service-definitions:
payment-gateway:
url: "https://payment-api.example.com"
type: payment-service-remote-factory # Optional, required for published-services
deployments:
app:
servers:
api-server:
# Framework auto-detects external services from URL
published-services:
- order-service # Depends on payment-gateway (external)
How it works:
- Define external service in
external-service-definitionswith URL - Add
typefield if you want to use inpublished-services(auto-router generation) - Add to
depends-oninservice-definitions - Framework automatically creates remote proxy
- Auto-resolves based on URL presence!
When to use type field:
- β
Required: If external service used in
published-services(auto-generate router) - β Not needed: If only accessed via
GetRemoteService()for client calls
Implementation with proxy.Service:
// Simple external service wrapper
type PaymentServiceRemote struct {
proxyService *proxy.Service
}
func NewPaymentServiceRemote(proxyService *proxy.Service) *PaymentServiceRemote {
return &PaymentServiceRemote{
proxyService: proxyService,
}
}
func (s *PaymentServiceRemote) CreatePayment(p *CreatePaymentParams) (*Payment, error) {
return proxy.CallWithData[*Payment](s.proxyService, "CreatePayment", p)
}
func (s *PaymentServiceRemote) Refund(p *RefundParams) (*Refund, error) {
return proxy.CallWithData[*Refund](s.proxyService, "Refund", p)
}
// Factory for remote service
func PaymentServiceRemoteFactory(deps map[string]any, config map[string]any) any {
return NewPaymentServiceRemote(
service.CastProxyService(config["remote"]),
)
}
// Register service type with metadata
lokstra_registry.RegisterServiceType(
"payment-service-remote-factory",
nil, PaymentServiceRemoteFactory,
deploy.WithResource("payment", "payments"),
deploy.WithConvention("rest"),
deploy.WithRouteOverride("Refund", "POST /payments/{id}/refund"),
)
π See: Example 06 - External Services for complete demo
3. Local + Remote Services (Business Logic)
Business services that can be deployed locally OR accessed remotely:
Examples:
- Business entities (
user-service,order-service,product-service) - Domain logic (
accounting-service,inventory-service) - Application services (
notification-service,report-service)
Characteristics:
- β Has local implementation (business logic + DB)
- β Has remote implementation (proxy for microservices)
- β
Published as router when local (via
published-services) - β Auto-generates HTTP endpoints from service methods
- β Interface abstraction for deployment flexibility
- Follow REST/RPC convention
How Auto-Publishing Works:
When a business service is listed in published-services, the framework:
- Reads metadata from service instance (if implements
ServiceMeta) - Auto-generates router using convention (REST/RPC)
- Creates HTTP endpoints for each public method
- Makes service accessible remotely via HTTP
service-definitions:
user-service:
type: user-service-factory
depends-on: [user-repository]
deployments:
microservice:
servers:
user-server:
published-services:
- user-service # β Framework auto-exposes UserService via HTTP
- Auto-generates router using convention (REST/RPC)
- Creates HTTP endpoints for each public method
- Makes service accessible remotely via HTTP
Example - User Service with REST convention:
// UserService methods:
type UserService struct {
DB *Database
}
// Optional: Implement ServiceMeta for custom routing
func (s *UserService) GetResourceName() (string, string) {
return "user", "users"
}
func (s *UserService) GetConventionName() string {
return "rest"
}
func (s *UserService) GetRouteOverride() autogen.RouteOverride {
return autogen.RouteOverride{
Custom: map[string]autogen.Route{
// Custom route for non-standard method
"Activate": {Method: "POST", Path: "/users/{id}/activate"},
},
}
}
func (s *UserService) GetByID(p *GetByIDParams) (*User, error) {
return s.DB.QueryOne("SELECT * FROM users WHERE id = ?", p.ID)
}
// Usage in same deployment
userService := lokstra_registry.GetService[*UserService]("user-service")
user, err := userService.GetByID(&GetByIDParams{ID: 123})
// β
Direct method call - fast!
Note: Implementing ServiceMeta is optional for local services. If not implemented, the framework uses metadata from service registration.
Remote Implementation:
// Simple remote service wrapper
type UserServiceRemote struct {
proxyService *proxy.Service
}
// Constructor receives proxy.Service from framework
func NewUserServiceRemote(proxyService *proxy.Service) *UserServiceRemote {
return &UserServiceRemote{
proxyService: proxyService,
}
}
// Method uses proxy.CallWithData for HTTP calls
func (s *UserServiceRemote) GetByID(p *GetByIDParams) (*User, error) {
return proxy.CallWithData[*User](s.proxyService, "GetByID", p)
}
// Factory for remote service (framework calls this)
func UserServiceRemoteFactory(deps map[string]any, config map[string]any) any {
return NewUserServiceRemote(
service.CastProxyService(config["remote"]),
)
}
// Metadata in RegisterServiceType (not in struct!)
lokstra_registry.RegisterServiceType(
"user-service-factory",
UserServiceFactory, UserServiceRemoteFactory,
deploy.WithResource("user", "users"),
deploy.WithConvention("rest"),
)
// Usage in different deployment
userRemote := lokstra_registry.GetService[*UserServiceRemote]("user-service-remote")
user, err := userRemote.GetByID(&GetByIDParams{ID: 123})
// β
HTTP call - transparent!
π See: Example 04 - Multi-Deployment for complete demo
Proxy Patterns
Lokstra provides two proxy patterns for different remote access scenarios:
proxy.Service - Convention-Based Remote Services
Use when:
- β Service follows REST/RPC convention
- β Need auto-routing from method names
- β Internal microservices or external APIs with standard patterns
- β Consistent API patterns across methods
Features:
- Auto-generates URLs from convention + metadata
CallWithData[T]()for type-safe calls- Route override support for custom endpoints
- Metadata-driven (resource, plural, convention)
- Framework auto-injects
proxy.Serviceviaconfig["remote"]
Example:
// Simple remote wrapper
type UserServiceRemote struct {
proxyService *proxy.Service
}
func NewUserServiceRemote(proxyService *proxy.Service) *UserServiceRemote {
return &UserServiceRemote{
proxyService: proxyService,
}
}
// Metadata from RegisterServiceType
lokstra_registry.RegisterServiceType(
"user-service-factory",
UserServiceFactory, UserServiceRemoteFactory,
deploy.WithResource("user", "users"),
deploy.WithConvention("rest"),
)
// Auto-generates: GET /users/{id}
func (s *UserServiceRemote) GetByID(p *GetByIDParams) (*User, error) {
return proxy.CallWithData[*User](s.proxyService, "GetByID", p)
}
π See: Example 04 and Example 06
proxy.Router - Direct HTTP Calls
Use when:
- β Quick access to external API without creating service
- β Non-standard or legacy API endpoints
- β One-off calls or simple integrations
- β Prototype/testing external APIs
- β Donβt need auto-routing or convention
Features:
- Direct HTTP calls to any endpoint
- No metadata or convention required
- Flexible for any REST API
- Type-safe response parsing
- Good for quick integrations
Example:
// Create router proxy
router := proxy.NewRouter("https://api.external.com")
// Direct calls - no service wrapper needed
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
// GET /users/123
user, err := proxy.CallRouter[*User](
router,
"GET",
"/users/{id}",
map[string]any{"id": 123},
)
// POST /users with body
newUser, err := proxy.CallRouter[*User](
router,
"POST",
"/users",
map[string]any{
"name": "John",
"email": "john@example.com",
},
)
When to use proxy.Router vs proxy.Service:
| Aspect | proxy.Router | proxy.Service |
|---|---|---|
| Setup | Minimal (just URL) | Service + metadata |
| Auto-routing | β Manual paths | β Convention-based |
| Type safety | β Response only | β Request + Response |
| Use case | Quick/simple calls | Structured services |
| Maintenance | Low effort | Better for large APIs |
| Best for | External APIs, prototyping | Microservices, internal APIs |
Example - When proxy.Router is better:
// Scenario: Quick weather API integration
// With proxy.Router (simple!)
weatherRouter := proxy.NewRouter("https://api.weather.com")
weather, err := proxy.CallRouter[*WeatherData](
weatherRouter,
"GET",
"/forecast/{city}",
map[string]any{"city": "Jakarta"},
)
// With proxy.Service (overkill!)
// Need: WeatherServiceRemote struct, metadata, factory, etc.
// Too much boilerplate for one-off call!
π See: Example 07 - Remote Router for complete demo
Key Concept: Interface Abstraction
Same interface, different implementation:
// Define interface
type IUserService interface {
GetByID(p *GetByIDParams) (*User, error)
List(p *ListParams) ([]*User, error)
}
// Local implements interface
type UserService struct { ... }
func (s *UserService) GetByID(...) (*User, error) { /* DB call */ }
// Remote implements interface
type UserServiceRemote struct { ... }
func (s *UserServiceRemote) GetByID(...) (*User, error) { /* HTTP call */ }
// OrderService doesn't know which one!
type OrderService struct {
Users IUserService // Could be local OR remote!
}
func (s *OrderService) CreateOrder(p *CreateParams) (*Order, error) {
// This works for BOTH local and remote!
user, err := s.Users.GetByID(&GetByIDParams{ID: p.UserID})
// ...
}
Published Services
Services that are exposed via HTTP endpoints:
# config.yaml
service-definitions:
user-service:
type: user-service-factory
depends-on: [user-repository]
deployments:
microservice:
servers:
user-server:
base-url: "http://localhost"
addr: ":3004"
published-services:
- user-service # β Makes UserService available via HTTP
What happens:
- Auto-router generated from service metadata
- Routes created for each service method
- HTTP endpoints available for remote calls
Example:
published-services: [user-service]
Auto-generates:
GET /users β UserService.List()
GET /users/{id} β UserService.GetByID()
POST /users β UserService.Create()
PUT /users/{id} β UserService.Update()
DELETE /users/{id} β UserService.Delete()
Service Resolution (Auto-Discovery)
Lokstra automatically resolves service locations:
service-definitions:
user-service:
type: user-service-factory
depends-on: [user-repository]
order-service:
type: order-service-factory
depends-on: [order-repository, user-service]
deployments:
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]
# No manual service listing needed!
How it works:
user-servicepublished athttp://localhost:3004order-servicedepends onIUserService(from service-definitions)- Lokstra auto-discovers:
user-serviceβhttp://localhost:3004 - Creates
user-service-remoteclient automatically - Injects remote client into OrderService
No manual URL configuration needed! β
Service Types Comparison
| Aspect | Local Service | Remote Service | Published Service |
|---|---|---|---|
| Execution | In-process | HTTP call | Exposes via HTTP |
| Performance | Fast (ns) | Slower (ms) | Serves HTTP requests |
| Usage | Same deployment | Different deployment | Makes service accessible |
| Code | Business logic | HTTP proxy | Business logic + router |
| Suffix | -service |
-service-remote |
-service |
Core Pattern: Service-Level Lazy Loading
type UserService struct {
DB *Database
Cache *CacheService
Email *EmailService
}
// Factory function - dependencies resolved eagerly when service created
func UserServiceFactory(deps map[string]any, config map[string]any) any {
return &UserService{
DB: deps["db"].(*Database),
Cache: deps["cache"].(*CacheService),
Email: deps["email"].(*EmailService),
}
}
// Registered in registry
lokstra_registry.RegisterServiceType("user-service-factory", UserServiceFactory, nil)
Service-level lazy = Service created only when first accessed, dependencies eager:
var userService = service.LazyLoad[*UserService]("user-service")
func handler() {
// UserService created here on first call (dependencies already loaded)
user, err := userService.MustGet().CreateUser(p)
}
func (s *UserService) CreateUser(p *CreateParams) (*User, error) {
// Direct access - dependencies already resolved when service was created
user, err := s.DB.Insert("INSERT INTO users ...")
if err != nil {
return nil, err
}
// Direct access to email service
s.Email.SendWelcome(user.Email)
return user, nil
}
Why Lazy Loading?
Problem - Circular Dependencies:
// Eager loading fails!
userService := &UserService{
Orders: orderService, // Not created yet!
}
orderService := &OrderService{
Users: userService, // Not created yet!
}
Solution - Lazy Loading:
// Lazy loading works!
userService := &UserService{
Orders: service.LazyLoad[*OrderService]("order-service"),
}
orderService := &OrderService{
Users: service.LazyLoad[*UserService]("user-service"),
}
// Both reference each other - resolved when .Get() is called
Service Method Requirements
MUST use struct parameters:
// β
RIGHT: Struct parameter
type GetByIDParams struct {
ID int `path:"id"`
}
func (s *UserService) GetByID(p *GetByIDParams) (*User, error) {
return s.DB.QueryOne("SELECT * FROM users WHERE id = ?", p.ID)
}
// β WRONG: Primitive parameter
func (s *UserService) GetByID(id int) (*User, error) {
// Can't bind from path/query/body!
}
Why? Lokstra uses struct tags to bind request data:
path:"id"- from URL pathquery:"name"- from query stringjson:"email"- from JSON bodyheader:"Authorization"- from headers
Deployment Flexibility
Same code, different topology:
service-definitions:
user-service:
type: user-service-factory
depends-on: [user-repository]
order-service:
type: order-service-factory
depends-on: [order-repository, user-service]
# Monolith: UserService is local
deployments:
monolith:
servers:
api-server:
base-url: "http://localhost"
addr: ":3003"
published-services:
- user-service
- order-service
# Framework auto-loads all dependencies:
# user-repository, order-repository, etc.
# Microservices: UserService is remote
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]
# OrderService.Users β UserServiceRemote (HTTP)
# Framework auto-detects from topology
OrderService code (unchanged):
type OrderService struct {
Users IUserService
}
func (s *OrderService) CreateOrder(p *CreateParams) (*Order, error) {
// In monolith: direct method call
// In microservice: HTTP call
user, err := s.Users.GetByID(&GetByIDParams{ID: p.UserID})
}
Key benefit: Deploy as monolith OR microservices without code changes!
π Learn more: Framework Guide
π Component 5: Middleware
Purpose: Request/response filters, cross-cutting concerns
Responsibilities
- β Logging
- β Authentication
- β CORS
- β Rate limiting
- β Request validation
- β Response transformation
Middleware Pattern
type MiddlewareFunc func(ctx *request.Context, next func() error) error
// Example: Logging middleware
func LoggingMiddleware() MiddlewareFunc {
return func(ctx *request.Context, next func() error) error {
start := time.Now()
// Before handler
log.Printf("β %s %s", ctx.R.Method, ctx.R.URL.Path)
// Execute next middleware/handler
err := next()
// After handler
duration := time.Since(start)
log.Printf("β %s %s (%v)", ctx.R.Method, ctx.R.URL.Path, duration)
return err
}
}
Middleware Chain Execution
Request
β
[Middleware 1] β before
β
[Middleware 2] β before
β
[Middleware 3] β before
β
[Handler] β execute
β
[Middleware 3] β after
β
[Middleware 2] β after
β
[Middleware 1] β after
β
Response
Example Flow
// Setup
r := lokstra.NewRouter("api")
r.Use(loggingMiddleware, corsMiddleware)
auth := r.Group("/admin")
auth.Use(authMiddleware)
auth.GET("/users", getUsers)
// Request: GET /admin/users
// Execution:
logging.before()
cors.before()
auth.before()
getUsers() // handler
auth.after()
cors.after()
logging.after()
Two Usage Methods
Method 1: Direct Function
r.Use(func(ctx *request.Context, next func() error) error {
// middleware logic
return next()
})
Method 2: By Name (Registry)
// Register
lokstra_registry.RegisterMiddleware("auth", authMiddleware)
lokstra_registry.RegisterMiddleware("logging", loggingMiddleware)
// Use by name (string)
r.Use("auth", "logging")
// Mix direct and by-name
r.Use(corsMiddleware, "auth")
π Learn more: Middleware Guide
βοΈ Component 6: Configuration
Purpose: Application settings and deployment topology management
Responsibilities
- β Load YAML config files
- β Service/Router/Middleware registration
- β Multi-deployment topology
- β Service auto-discovery and resolution
Configuration Structure
# config.yaml
# ========================================
# Service Definitions (Global)
# ========================================
service-definitions:
user-repository:
type: user-repository-factory
depends-on: [db-service]
user-service:
type: user-service-factory
depends-on: [user-repository]
order-repository:
type: order-repository-factory
depends-on: [db-service]
order-service:
type: order-service-factory
depends-on: [order-repository, user-service]
db-service:
type: database-factory
# ========================================
# Deployments (Topology)
# ========================================
deployments:
# Monolith: All services in one process
monolith:
servers:
api-server:
base-url: "http://localhost"
addr: ":3003"
published-services:
- user-service
- order-service
# Framework resolves dependency chains:
# user-service β [user-repository] β [db-service]
# order-service β [order-repository, user-service] β [db-service]
# All created lazily when first accessed
# Microservices: Each service in separate process
microservice:
servers:
user-server:
base-url: "http://localhost"
addr: ":3004"
published-services: [user-service]
# Resolves: user-service β [user-repository] β [db-service]
order-server:
base-url: "http://localhost"
addr: ":3005"
published-services: [order-service]
# Resolves: order-service β [order-repository, user-service-remote]
# order-repository β [db-service]
# Auto-detects: user-service from user-server
Key improvements:
- β
No
required-services- framework resolves dependency chains automatically - β
No
required-remote-services- framework auto-detects remote services - β Lazy loading - services created only when first accessed
- β
Zero-config - just list
published-services!
Key Configuration Concepts
1. Service Definitions (Global)
Define services once, use in multiple deployments:
service-definitions:
user-service:
type: user-service-factory
depends-on: [user-repository]
What it does:
- Registers service factory in global registry
- Declares dependencies
- Available to all deployments
2. Deployments (Topology)
Define how services are distributed across servers:
deployments:
monolith: # All-in-one
microservice: # Distributed
Each deployment is independent topology
3. Zero-Config Service Resolution
No manual service listing needed!
The framework automatically:
- β
Loads all dependencies from
service-definitions - β Detects remote services from published services in other servers
- β Creates local or remote instances based on availability
service-definitions:
order-repository:
type: order-repository-factory
depends-on: [db-service]
order-service:
type: order-service-factory
depends-on: [order-repository, user-service]
user-service:
type: user-service-factory
depends-on: [user-repository]
deployments:
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 automatically:
# - Loads order-repository (local)
# - Detects user-service published on user-server
# - Creates user-service-remote proxy
4. Published Services
Services exposed via HTTP endpoints:
servers:
user-server:
published-services:
- user-service # Creates HTTP endpoints automatically
What happens:
- HTTP routes created for each method
- Service becomes accessible remotely
5. Service Auto-Discovery
Framework automatically discovers service locations from topology!
service-definitions:
user-service:
type: user-service-factory
depends-on: [user-repository]
order-service:
type: order-service-factory
depends-on: [order-repository, user-service]
deployments:
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]
# No manual service listing needed!
# Framework auto-detects user-service from topology
Lokstra automatically:
- Reads
order-servicedependencies from itsservice-definitions - Finds
user-servicepublished athttp://localhost:3004 - Auto-creates
user-service-remoteβhttp://localhost:3004 - Injects remote client into OrderService
Multi-Deployment Architecture
Key concept: Same code, different deployment configurations
Example - User microservice:
# Monolith deployment
go run . -server=monolith
# Loads: user-service (local) + order-service (local)
# User microservice deployment
go run . -server=user-service
# Loads: user-service (local only)
# Order microservice deployment
go run . -server=order-service
# Loads: order-service (local) + user-service-remote (HTTP)
How it works:
// OrderService code (unchanged)
type OrderService struct {
Users IUserService // Interface!
}
func (s *OrderService) CreateOrder(p *CreateParams) (*Order, error) {
// In monolith: direct method call
// In microservice: HTTP call
user, err := s.Users.GetByID(&GetByIDParams{ID: p.UserID})
}
Deployment determines implementation:
- Monolith:
UsersβUserService(local) - Microservice:
UsersβUserServiceRemote(HTTP)
Configuration Loading
// Load config
config, err := loader.LoadConfig("config.yaml")
// Build deployment topology
err = loader.LoadAndBuild([]string{"config.yaml"})
// Get topology for specific deployment
registry := deploy.Global()
topology := registry.GetDeploymentTopology("microservice")
// Build server from topology
server, err := registry.BuildServer("microservice", "order-server")
External Service Definitions
For services outside your deployment (external APIs):
external-service-definitions:
payment-gateway-remote:
url: "https://payment-api.example.com"
type: payment-service-remote-factory # Optional: only if used in published-services
email-service-remote:
url: "https://email.example.com"
# No type field - only accessed via GetRemoteService()
Field descriptions:
url(required): Base URL of external servicetype(optional): Factory type for auto-creating service wrapper- β
Required if used in
published-services(auto-router generation) - β Not needed if only accessed via
GetRemoteService()
- β
Required if used in
resource,resource-plural,convention,overrides: Override metadata
Use case: Third-party services not in your topology
π Learn more: Framework Guide
π Complete Request Flow
Letβs trace a request through all components:
Example Setup
// 1. Register services
lokstra_registry.RegisterServiceFactory("db", createDB)
lokstra_registry.RegisterServiceFactory("users", func() any {
return &UserService{DB: service.LazyLoad[*Database]("db")}
})
// 2. Create router
r := lokstra.NewRouter("api")
r.Use(loggingMiddleware)
auth := r.Group("/admin")
auth.Use(authMiddleware)
auth.GET("/users/{id}", getUser)
// 3. Create app
app := lokstra.NewApp("api", ":8080", r)
// 4. Create server
server := &Server{Apps: []*App{app}}
server.Run(30 * time.Second)
Request: GET /admin/users/123
Step 1: TCP Connection
Client β App (port 8080)
Step 2: App receives request
App.ServeHTTP(w, req)
β
Router.ServeHTTP(w, req)
Step 3: Router matches route
Method: GET β
Path: /admin/users/{id} β
Extract params: {id: "123"}
Step 4: Build middleware chain
Global: [loggingMiddleware]
Group: [authMiddleware]
Route: []
Chain: [logging, auth]
Step 5: Create context
ctx := request.NewContext(w, req)
ctx.PathParams["id"] = "123"
Step 6: Execute chain
logging.before()
β Log: "GET /admin/users/123"
auth.before()
β Check: Authorization header
β Validate: JWT token
handler.execute()
β Call: getUser(ctx)
β Extract: id from ctx.PathParams
β Service: userService.GetByID(id)
β DB: SELECT * FROM users WHERE id = 123
β Response: user object
auth.after()
β (nothing)
logging.after()
β Log: "200 OK (45ms)"
Step 7: Write response
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 123, "name": "John", "email": "john@example.com"}
ποΈ Architecture Patterns
Pattern 1: Layered Architecture
βββββββββββββββββββββββββββββββββββββββ
β Presentation Layer β
β (Router, Middleware, Handlers) β
βββββββββββββββββββββββββββββββββββββββ€
β Business Layer β
β (Services) β
βββββββββββββββββββββββββββββββββββββββ€
β Data Layer β
β (Database, Cache, APIs) β
βββββββββββββββββββββββββββββββββββββββ
Example:
// Presentation: Handler
func GetUserHandler(ctx *request.Context) (*User, error) {
id := ctx.PathParam("id")
return userService.GetByID(id) // Call business layer
}
// Business: Service
func (s *UserService) GetByID(id string) (*User, error) {
return s.DB.MustGet().QueryOne(...) // Call data layer
}
// Data: Database
func (db *Database) QueryOne(query string) (*User, error) {
// Execute SQL
}
Pattern 2: Dependency Injection
Registry (Central)
β
Services ββββ Lazy Load
β
Handlers
Example:
// Registry
lokstra_registry.RegisterServiceFactory("users", createUserService)
// Service with dependencies
type UserService struct {
DB *Database
Email *EmailService
}
// Handler uses service
userService := lokstra_registry.GetService[*UserService]("users")
Pattern 3: Convention over Configuration
Example: Service as Router
Instead of:
// Configuration approach
r.GET("/users", listUsers)
r.GET("/users/{id}", getUser)
r.POST("/users", createUser)
r.PUT("/users/{id}", updateUser)
r.DELETE("/users/{id}", deleteUser)
Use:
// Convention approach
router := router.NewFromService(userService, "/users")
// Auto-generates routes based on method names
π― Design Principles
1. Separation of Concerns
- Router: Routing only
- Middleware: Cross-cutting concerns
- Handler: Request/response
- Service: Business logic
- Configuration: Settings
2. Dependency Inversion
- High-level (handlers) depend on abstractions (services)
- Low-level (databases) implement abstractions
- Lazy loading for flexible resolution
3. Convention over Configuration
- Standard method names β Routes
- Struct tags β Parameter binding
- Sensible defaults
4. Flexibility
- 29 handler forms
- Multiple deployment modes
- Code or config-driven
5. Type Safety
- Generics for services
- Compile-time checks
- No reflection in hot path
π Component Interaction Diagram
ββββββββββββ
β Client β
ββββββ¬ββββββ
β HTTP Request
β
ββββββββββββββββββββββββββββββββββββββββββββ
β SERVER β
β (Lifecycle, Graceful Shutdown) β
β β
β βββββββββββββββββββββββββββββββββββββββ β
β β APP β β
β β (HTTP Listener) β β
β β β β
β β ββββββββββββββββββββββββββββββββββ β β
β β β ROUTER β β β
β β β β β β
β β β Match Route β β β
β β β β β β β
β β β ββββββββββββββββββββββββββ β β β
β β β β MIDDLEWARE CHAIN β β β β
β β β β [MW1] β [MW2] β [MW3] β β β β
β β β ββββββββββββ¬ββββββββββββββ β β β
β β β β β β β
β β β ββββββββββββββββββββββββββ β β β
β β β β HANDLER β β β β
β β β β (Extract params) β β β β
β β β ββββββββββββ¬ββββββββββββββ β β β
β β βββββββββββββββΌβββββββββββββββββββ β β
β ββββββββββββββββββΌβββββββββββββββββββββ β
βββββββββββββββββββββΌβββββββββββββββββββββββ
β
ββββββββββββββββββββββββββ
β SERVICE β
β (Business Logic) β
β β
β ββββββββββββββββββββ β
β β Dependencies β β
β β (Lazy Load) β β
β β β β
β β DB, Cache, etc β β
β ββββββββββββββββββββ β
ββββββββββ¬ββββββββββββββββ
β
ββββββββββββββββββββββββββ
β External Resources β
β (Database, APIs, etc) β
ββββββββββββββββββββββββββ
π‘ Key Takeaways
- Server: Container, manages lifecycle, NOT in request flow
- App: HTTP listener, serves router
- Router: Route matching, middleware orchestration
- Middleware: Request/response filters, cross-cutting concerns
- Service: Business logic, lazy-loaded dependencies
- Configuration: Settings, multi-deployment support
Request Flow:
App β Router β Middleware Chain β Handler β Service β Response
Dependency Flow:
Registry β Lazy Services β Handlers/Services β External Resources
π Learn More
Next Steps:
- Router Guide - Hands-on tutorials
- Framework Guide - Advanced patterns
- API Reference - Complete API docs
Specific Components:
Ready to start building? π Quick Start