Key Features
What makes Lokstra special - the features that set it apart
π― Overview
Lokstra has several killer features that make building REST APIs faster, cleaner, and more flexible:
- 29 Handler Forms - Write handlers your way
- Service as Router - Auto HTTP endpoints from services
- One Binary, Multiple Deployments - Monolith β Microservices
- Built-in Lazy DI - No external framework needed
- Flexible Configuration - Code + YAML patterns
- Annotation-Driven Development - 83% less boilerplate
Letβs dive into each feature:
π¨ Feature 1: 29 Handler Forms
The Problem
Most frameworks lock you into one handler pattern:
// Gin - must use this signature
func Handler(c *gin.Context) {
// forced pattern
}
// Echo - must use this
func Handler(c echo.Context) error {
// forced pattern
}
The Lokstra Solution
29 different handler forms - use what makes sense:
Form 1: Simplest - No Params, Return Value
r.GET("/ping", func() string {
return "pong"
})
r.GET("/config", func() map[string]any {
return map[string]any{
"version": "1.0",
"env": "production",
}
})
When: Simple endpoints, no errors possible
Form 2: With Error Handling (Most Common!)
r.GET("/users", func() ([]User, error) {
users, err := db.GetAllUsers()
if err != nil {
return nil, err // Auto 500 error
}
return users, nil // Auto 200 OK
})
When: Operations that can fail (90% of cases)
Form 3: With Request Binding
type CreateUserRequest struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
r.POST("/users", func(req *CreateUserRequest) (*User, error) {
// req auto-bound from JSON body
// auto-validated
return db.CreateUser(req.Name, req.Email)
})
When: Need request data (POST/PUT)
Form 4: Full Control
r.GET("/complex", func(ctx *request.Context) (*response.Response, error) {
// Access everything
token := ctx.R.Header.Get("Authorization")
userAgent := ctx.R.Header.Get("User-Agent")
// Custom response
return response.Success(data).
WithHeader("X-Custom", "value").
WithStatus(201), nil
})
When: Need headers, cookies, custom status codes
Why This Matters
Developer Experience:
// Simple case? Simple code!
r.GET("/ping", func() string { return "pong" })
// Complex case? Full power!
r.GET("/api", func(ctx *request.Context, req *ComplexRequest) (*response.Response, error) {
// Do complex stuff
})
One size doesnβt fit all - Lokstra adapts to your needs.
π See all 29 forms: Deep Dive: Handler Forms
β‘ Feature 2: Service as Router
The Problem
Repetitive routing code for CRUD operations:
// Traditional approach - lots of boilerplate
r.GET("/users", listUsers)
r.GET("/users/{id}", getUser)
r.POST("/users", createUser)
r.PUT("/users/{id}", updateUser)
r.DELETE("/users/{id}", deleteUser)
// And handlers calling services...
func listUsers() ([]User, error) {
return userService.GetAll()
}
func getUser(req *GetUserReq) (*User, error) {
return userService.GetByID(req.ID)
}
// ... more boilerplate
The Lokstra Solution
Service methods automatically become HTTP endpoints:
// 1. Define service with methods
type UserService struct {
DB *Database
}
type GetAllParams struct {}
type GetByIDParams struct {
ID int `path:"id"`
}
type CreateParams struct {
Name string `json:"name"`
Email string `json:"email"`
}
func (s *UserService) GetAll(p *GetAllParams) ([]User, error) {
return s.DB.Query("SELECT * FROM users")
}
func (s *UserService) GetByID(p *GetByIDParams) (*User, error) {
return s.DB.QueryOne("SELECT * FROM users WHERE id = ?", p.ID)
}
func (s *UserService) Create(p *CreateParams) (*User, error) {
return s.DB.Insert("INSERT INTO users ...", p.Name, p.Email)
}
// 2. Register service factory
func UserServiceFactory(deps map[string]any, config map[string]any) any {
return &UserService{
DB: deps["db"].(*Database),
}
}
lokstra_registry.RegisterServiceType("users-factory", UserServiceFactory, nil)
// 3. Auto-generate router from service
userRouter := router.NewFromService(
lokstra_registry.GetService[*UserService]("users"),
"/users", // Base path
)
Result - Automatic routes:
GET /users β UserService.GetAll()
GET /users/{id} β UserService.GetByID()
POST /users β UserService.Create()
PUT /users/{id} β UserService.Update()
DELETE /users/{id} β UserService.Delete()
Convention-Based Routing
Lokstra understands REST conventions:
| Method Name | HTTP Method | Path | Parameters |
|---|---|---|---|
GetAll() |
GET | / |
None |
GetByID() |
GET | /{id} |
ID from path |
Create() |
POST | / |
Body |
Update() |
PUT | /{id} |
ID + Body |
Delete() |
DELETE | /{id} |
ID from path |
List() |
GET | / |
Query params |
Search() |
GET | /search |
Query params |
Why This Matters
Zero Boilerplate:
- β No handler functions needed
- β No routing registration
- β No parameter extraction
- β Focus on business logic only
Business Logic in Services:
// All your logic in one place
type UserService struct {
DB *Database
Email *EmailService
Cache *CacheService
}
// Pure business logic
func (s *UserService) Create(p *CreateParams) (*User, error) {
// Validate
if err := validateUserData(p); err != nil {
return nil, err
}
// Create - direct access, dependencies already injected
user, err := s.DB.Insert(...)
if err != nil {
return nil, err
}
// Cache user data
s.Cache.Set(user.ID, user)
// Notify
s.Email.SendWelcome(user.Email)
return user, nil
}
π Learn more: Framework Guide
ποΈ Feature 3: One Binary, Multiple Deployments
The Problem
Migrating from monolith to microservices requires:
- Code refactoring
- New repos
- Different builds
- Deployment complexity
The Lokstra Solution
Same code, different deployment configurations:
# config.yaml - One config file, multiple deployments
servers:
# Deployment 1: Monolith (all services in one)
- name: monolith
deployment-id: monolith
apps:
- addr: ":8080"
services: [users, orders, payments, products]
# Deployment 2: Microservices (split services)
- name: user-service
deployment-id: microservices
base-url: http://user-service
apps:
- addr: ":8001"
services: [users]
- name: order-service
deployment-id: microservices
base-url: http://order-service
apps:
- addr: ":8002"
services: [orders, payments]
- name: product-service
deployment-id: microservices
base-url: http://product-service
apps:
- addr: ":8003"
services: [products]
One binary, multiple modes:
# Build once
go build -o myapp
# Deploy as monolith
./myapp --server=monolith
# Or deploy as microservices
./myapp --server=user-service # Instance 1
./myapp --server=order-service # Instance 2
./myapp --server=product-service # Instance 3
How It Works
Deployment Isolation:
- Services in same
deployment-idcan communicate - Services in different
deployment-idare isolated - Lokstra handles routing automatically
Example:
// In OrderService
type OrderService struct {
Users *UserService // May be local or remote!
}
func (s *OrderService) CreateOrder(p *CreateOrderParams) (*Order, error) {
// This works in BOTH deployments!
user, err := s.Users.GetByID(p.UserID)
// Monolith: Direct call
// Microservices: HTTP call to user-service
// ... create order
}
Why This Matters
Flexibility:
- Start as monolith (simple deployment)
- Split to microservices when needed (no code change)
- Test locally as monolith
- Deploy as microservices
Cost Efficiency:
- Small projects: One server
- Growing: Split services gradually
- Enterprise: Full microservices
Development Speed:
- Local dev: Monolith (fast iteration)
- Staging: Multi-port (test separation)
- Production: Microservices (scale independently)
π See it in action: Multi-Deployment Example
π§ Feature 4: Built-in Lazy Dependency Injection
The Problem
Managing dependencies manually or using heavy DI frameworks:
// Manual DI - order matters, error-prone
db := createDB()
cache := createCache()
userRepo := NewUserRepo(db, cache) // Must create in order!
orderRepo := NewOrderRepo(db, cache)
userService := NewUserService(userRepo)
orderService := NewOrderService(orderRepo, userService)
The Lokstra Solution
Built-in lazy DI with type safety:
import "github.com/primadi/lokstra/core/service"
// 1. Define services with dependencies
type OrderService struct {
DB *service.Cached[*Database]
Users *service.Cached[*UserService]
Cache *service.Cached[*CacheService]
}
// 2. Register factories (order doesn't matter with depends-on!)
lokstra_registry.RegisterServiceType("db", createDatabase)
lokstra_registry.RegisterServiceType("cache", createCache)
lokstra_registry.RegisterServiceFactory("users-factory",
func(deps map[string]any, config map[string]any) any {
return &UserService{
DB: service.Cast[*Database](deps["db"]),
}
})
lokstra_registry.RegisterLazyService("users", "users-factory",
map[string]any{"depends-on": []string{"db"}})
lokstra_registry.RegisterServiceFactory("orders-factory",
func(deps map[string]any, config map[string]any) any {
return &OrderService{
DB: service.Cast[*Database](deps["db"]),
Users: service.Cast[*UserService](deps["users"]),
Cache: service.Cast[*CacheService](deps["cache"]),
}
})
lokstra_registry.RegisterLazyService("orders", "orders-factory",
map[string]any{"depends-on": []string{"db", "users", "cache"}})
// 3. Use anywhere - dependencies auto-injected
orders := lokstra_registry.GetService[*OrderService]("orders")
Lazy Loading
Services created only when first accessed:
type OrderService struct {
DB *service.Cached[*Database]
}
func (s *OrderService) CreateOrder(p *CreateOrderParams) (*Order, error) {
// DB created on first .Get() call
db := s.DB.Get() // Lazy load here!
return db.Insert("INSERT INTO orders ...")
}
Benefits:
- β Services created only if used
- β Circular dependencies OK (lazy resolution)
- β Faster startup (no upfront init)
- β Memory efficient
Type-Safe with Generics
// Type-safe - compile-time checked
users := lokstra_registry.GetService[*UserService]("users")
// β
Type: *UserService
// Wrong type? Compile error!
users := lokstra_registry.GetService[*WrongType]("users")
// β Compile error
Why This Matters
No External Framework:
- No uber/dig
- No google/wire
- Built into Lokstra
Simple API:
// Register
RegisterServiceType(name, factory)
// Use
GetService[T](name)
// That's it!
π Learn more: Framework Guide
βοΈ Feature 5: Flexible Configuration
The Problem
Hardcoded configuration or limited config options:
// Hardcoded - must recompile to change
const port = ":8080"
const dbHost = "localhost"
The Lokstra Solution
Code + YAML pattern - best of both worlds:
Approach 1: Pure Code (Simple Apps)
r := lokstra.NewRouter("api")
r.Use(logging.Middleware(), cors.Middleware())
r.GET("/users", getUsers)
app := lokstra.NewApp("demo", ":8080", r)
app.Run(30 * time.Second)
Approach 2: Code + Config (Recommended)
// code: setup.go
func setupServices() {
lokstra_registry.RegisterServiceType("db", createDB)
lokstra_registry.RegisterServiceType("users", createUserService)
}
func setupRouters() {
lokstra_registry.RegisterRouter("api", createAPIRouter())
}
// config: app.yaml
servers:
- name: dev-server
deployment-id: dev
apps:
- addr: ":3000"
routers: [api]
services: [db, users]
# main.go
func main() {
setupServices()
setupRouters()
var cfg config.Config
config.LoadConfigFile("app.yaml", &cfg)
lokstra_registry.RegisterConfig(&cfg, "dev-server")
lokstra_registry.RunServer(30 * time.Second)
}
Environment Variables
# config.yaml
services:
- name: database
type: postgres
config:
host: ${DB_HOST:localhost} # env var with default
port: ${DB_PORT:5432}
password: ${DB_PASSWORD} # required env var
ssl_mode: ${DB_SSL_MODE:disable}
Multi-File Configuration
# base.yaml - shared config
services:
- name: database
type: postgres
# dev.yaml - dev overrides
services:
- name: database
config:
host: localhost
# prod.yaml - production overrides
services:
- name: database
config:
host: ${DB_HOST}
ssl_mode: require
Load and merge:
var cfg config.Config
config.LoadConfigFile("base.yaml", &cfg)
config.LoadConfigFile("dev.yaml", &cfg) // Merges with base
Why This Matters
Flexibility:
- Simple apps: Pure code
- Complex apps: Code + config
- Enterprise: Full config-driven
Environment Management:
- One codebase
- Multiple environments
- Easy deployment
π Learn more: Framework Guide
οΏ½ Feature 6: Annotation-Driven Development
The Problem
Setting up services with DI, routing, and wiring requires lots of boilerplate:
// 70+ lines of manual setup for one service!
// 1. Define service
type UserService struct {
DB *service.Cached[*Database]
}
// 2. Create factory
func createUserServiceFactory() any {
return func(deps map[string]any, config map[string]any) any {
return &UserService{
DB: service.Cast[*Database](deps["db"]),
}
}
}
// 3. Register factory
lokstra_registry.RegisterServiceFactory("user-service-factory",
createUserServiceFactory())
// 4. Register lazy service
lokstra_registry.RegisterLazyService("user-service",
"user-service-factory",
map[string]any{"depends-on": []string{"db"}})
// 5. Create router from service
func setupUserRouter() *lokstra.Router {
userService := lokstra_registry.GetService[*UserService]("user-service")
return router.NewFromService(userService, "/api")
}
// 6. Register router
lokstra_registry.RegisterRouter("user-router", setupUserRouter())
// 7. Mount in server config
// ... more YAML config
The Lokstra Solution
Annotations replace 70+ lines with 12 lines - like NestJS decorators:
// @RouterService name="user-service", prefix="/api"
type UserServiceImpl struct {
// @Inject "database"
DB *service.Cached[*Database]
}
// @Route "GET /users"
func (s *UserServiceImpl) GetAll(p *GetAllRequest) ([]User, error) {
return s.DB.MustGet().GetAllUsers()
}
// @Route "GET /users/{id}"
func (s *UserServiceImpl) GetByID(p *GetByIDRequest) (*User, error) {
return s.DB.MustGet().GetUserByID(p.ID)
}
// Auto-generates: factory, DI wiring, routes, remote proxy!
How It Works
Step 1: Add Annotations
// @RouterService name="user-service", prefix="/api", mount="/api"
type UserServiceImpl struct {
// @Inject "database"
DB *service.Cached[*Database]
}
// @Route "POST /users"
func (s *UserServiceImpl) Create(p *CreateUserRequest) (*User, error) {
return s.DB.MustGet().CreateUser(p)
}
Step 2: Run to Auto-Generate
# Code generation happens automatically
go run .
Step 3: Lokstra Generates Everything
Generates zz_generated.lokstra.go:
// β
Service factory
func init() {
lokstra_registry.RegisterServiceFactory("user-service-factory", ...)
lokstra_registry.RegisterLazyService("user-service", ...)
}
// β
Router with routes
func init() {
r := lokstra.NewRouter("user-service")
r.POST("/users", ...) // Auto-wired to Create()
lokstra_registry.RegisterRouter("user-service", r)
}
// β
Remote service proxy (for microservices)
type UserServiceRemote struct { ... }
Three Powerful Annotations
1. @RouterService - Define Service
// @RouterService name="user-service", prefix="/api", mount="/api"
type UserServiceImpl struct {}
Generates:
- Service factory
- DI registration
- Router with prefix
- Remote proxy for microservices
2. @Inject - Dependency Injection
type UserServiceImpl struct {
// @Inject "database"
DB *service.Cached[*Database]
// @Inject "email-service"
Email *service.Cached[*EmailService]
}
Generates:
- Dependency wiring in factory
depends-onconfiguration- Type-safe lazy loading
3. @Route - HTTP Endpoints
// @Route "GET /users"
func (s *UserServiceImpl) GetAll(p *GetAllRequest) ([]User, error) {}
// @Route "POST /users"
func (s *UserServiceImpl) Create(p *CreateUserRequest) (*User, error) {}
// @Route "PUT /users/{id}"
func (s *UserServiceImpl) Update(p *UpdateUserRequest) (*User, error) {}
Generates:
- Route registration
- Parameter binding (path, query, body)
- HTTP method mapping
- Response serialization
Why This Matters
Massive Code Reduction:
Traditional Approach: 70+ lines of boilerplate
With Annotations: 12 lines of business logic
Reduction: 83% less code!
Developer Experience:
- β Like NestJS decorators (familiar pattern)
- β Like Spring annotations (proven approach)
- β But in Go (compile-time safe)
- β No reflection at runtime (fast!)
Productivity Boost:
// Before: Write 6 separate files
service.go // Service implementation
factory.go // Factory function
register.go // Registration code
router.go // Router setup
routes.go // Route definitions
config.yaml // YAML configuration
// After: Write 1 annotated file
user_service.go // Everything in one place!
Type Safety:
- Annotations validated at build time
- Generated code is type-safe
- No runtime reflection overhead
- Full IDE support
Comparison with Other Frameworks
| Framework | Pattern | Runtime Cost |
|---|---|---|
| NestJS | Decorators | High (reflection) |
| Spring | Annotations | High (reflection) |
| Lokstra | Annotations | Zero (code generation) |
Lokstra advantage: All the DX benefits of annotations, none of the runtime cost!
Build Workflow Integration
Problem: Code generation only happens during go run, not go build
Solution: Use --generate-only flag:
# Force code generation without running
go run . --generate-only
# Then build normally
go build -o myapp
Or use provided build scripts:
# Linux/Mac
./build.sh
# Windows (PowerShell)
.\build.ps1
# Windows (CMD)
.\build.bat
Scripts automatically:
- Run
go run . --generate-only - Run
go mod tidy - Build for multiple platforms
Real-World Example
See Example 07: Enterprise Router Service:
- Complete working example
- Full documentation
- Before/after comparison
- Build scripts included
π Full guide: Example 07
Feature Comparison
| Feature | Standard Libs | Gin/Echo | Lokstra |
|---|---|---|---|
| Handler Forms | 1 | 1 | 29 |
| Service Layer | Manual | Manual | Built-in |
| DI System | None | None | Built-in (Lazy) |
| ServiceβRouter | No | No | Yes (Auto) |
| Multi-Deploy | No | No | Yes (1 binary) |
| Config-Driven | Limited | Limited | Full (YAML) |
| Lazy Loading | Manual | Manual | Built-in |
| Convention/Config | Config | Config | Convention + Config |
| Annotations | No | No | Yes (83% less code) |
π See Features in Action
Quick Demos:
Handler Forms: π Handler Forms Example
Service as Router: π Service Router Example
Multi-Deployment: π Single Binary Example
Full Stack: π Complete Todo API
π‘ What Makes Lokstra Unique?
Most frameworks focus on one thing:
- Fast routing (Chi)
- Simple API (Gin)
- Full features (Beego)
Lokstra focuses on developer experience:
- β Flexible (29 handler forms)
- β Productive (service as router)
- β Scalable (multi-deployment)
- β Clean (built-in DI)
- β Simple (convention over config)
- β Modern (annotation-driven like NestJS)
Result: Build faster, scale easier, maintain better.
π Learn More
Deep Dives:
- Architecture - How it all works together
- Router Guide - Hands-on tutorials
- Deep Dive - Advanced patterns
Try It:
- Quick Start - Build your first API in 5 minutes
- Examples - Real applications
Ready to experience these features? π Get Started