Lokstra Enterprise Router Service Template
Annotation-Driven Router Services with Auto-Generated Code
This template demonstrates how to use Lokstra Annotations to automatically generate router services, eliminating boilerplate code and streamlining development. This is the recommended approach for building enterprise applications with Lokstra.
π Table of Contents
- What are Lokstra Annotations?
- When to Use This Template
- Key Features
- Architecture Overview
- Project Structure
- Lokstra Annotations Reference
- How It Works
- Getting Started
- Adding New Modules
- Code Generation
- Deployment Strategies
- Comparison with Manual Registration
π What are Lokstra Annotations?
Lokstra Annotations are special comments in your Go code that automatically generate:
- β Service factories
- β Remote service proxies
- β Router configurations
- β Dependency injection wiring
- β Service registrations
Write this:
// @RouterService name="user-service", prefix="/api"
type UserServiceImpl struct {
// @Inject "user-repository"
UserRepo *service.Cached[domain.UserRepository]
}
// @Route "GET /users/{id}"
func (s *UserServiceImpl) GetByID(p *domain.GetUserRequest) (*domain.User, error) {
return s.UserRepo.MustGet().GetByID(p.ID)
}
Get this auto-generated:
- Service factory function
- Remote HTTP proxy implementation
- Router registration with endpoints
- Dependency injection wiring
- Service type registration
No more manual boilerplate! π
π― When to Use This Template
Use this template when you want:
- β Rapid development - Annotations eliminate boilerplate
- β Type-safe DI - Auto-detected dependencies
- β Auto-generated routers - Routes from method signatures
- β Microservice-ready - Automatic remote proxies
- β Clean code - Business logic without framework noise
- β Modular architecture - Domain-Driven Design structure
Donβt use this template if:
- You need full control over service registration
- Your project is extremely simple (< 3 services)
- You prefer explicit configuration over code generation
β¨ Key Features
1. Zero Boilerplate
- No manual factory functions
- No router registration code
- No dependency wiring code
- Everything auto-generated from annotations
2. Type-Safe Development
- Compile-time type checking
- IDE autocomplete for annotations
- Auto-detected dependency types
- Refactoring-friendly
3. Hot Reload in Dev Mode
- Automatic code regeneration on file changes
- No manual rebuild needed
- Instant feedback loop
- Works with debugger
4. Production Ready
- Generated code is readable and debuggable
- No runtime reflection overhead
- Cached and optimized
- Same performance as hand-written code
π Architecture Overview
Annotation-Driven Architecture
This template uses three core annotations:
| Annotation | Purpose | Example |
|---|---|---|
@RouterService |
Marks a service to be published as HTTP router | @RouterService name="user-service" |
@Inject |
Auto-wires dependencies | @Inject "user-repository" |
@Route |
Maps methods to HTTP endpoints | @Route "GET /users/{id}" |
Three-Layer Architecture per Module
modules/{module-name}/
βββ domain/ # Business entities and interfaces
βββ application/ # Service implementation with annotations
β βββ user_service.go # @RouterService, @Inject, @Route
β βββ zz_generated.lokstra.go # Auto-generated
βββ infrastructure/ # Data access implementations
Each layer has specific responsibilities:
| Layer | Responsibility | Annotations |
|---|---|---|
| Domain | Business entities and rules | None |
| Application | Service with annotations | @RouterService, @Inject, @Route |
| Infrastructure | Repository implementations | None |
π Project Structure
01_enterprise_router_service/
βββ modules/ # Business modules
β βββ user/ # User management module
β β βββ domain/
β β β βββ entity.go # User entities
β β β βββ service.go # UserService interface
β β β βββ dto.go # Request/Response types
β β βββ application/
β β β βββ user_service.go # β¨ With annotations
β β β βββ zz_generated.lokstra.go # π€ Auto-generated
β β βββ infrastructure/
β β βββ repository/
β β βββ user_repository.go
β β
β βββ order/ # Order management module
β β βββ ... (same structure)
β β
β βββ shared/ # Shared kernel
β
βββ main.go # Entry point with lokstra.Bootstrap()
βββ register.go # Module registration (minimal)
βββ README.md
Key Files
main.go - Application entry point:
func main() {
lokstra.Bootstrap() // β¨ Magic happens here!
registerServiceTypes()
registerMiddlewareTypes()
lokstra_registry.RunServerFromConfigFolder("config")
}
user_service.go - Annotated service:
// @RouterService name="user-service", prefix="/api"
type UserServiceImpl struct {
// @Inject "user-repository"
UserRepo *service.Cached[domain.UserRepository]
}
// @Route "GET /users/{id}"
func (s *UserServiceImpl) GetByID(p *domain.GetUserRequest) (*domain.User, error) {
return s.UserRepo.MustGet().GetByID(p.ID)
}
zz_generated.lokstra.go - Auto-generated code:
// AUTO-GENERATED - DO NOT EDIT
func init() {
RegisterUserServiceImpl() // Auto-registers on import
}
func UserServiceImplFactory(deps map[string]any, config map[string]any) any {
return &UserServiceImpl{
UserRepo: service.Cast[domain.UserRepository](deps["user-repository"]),
}
}
func RegisterUserServiceImpl() {
lokstra_registry.RegisterServiceType("user-service-factory",
UserServiceImplFactory,
UserServiceImplRemoteFactory,
deploy.WithRouter(&deploy.ServiceTypeRouter{
PathPrefix: "/api",
CustomRoutes: map[string]string{
"GetByID": "GET /users/{id}",
// ... all routes auto-detected
},
}),
)
}
register.go - Simple module loading:
func registerServiceTypes() {
user.Register() // Triggers package init() -> auto-registration
order.Register() // Triggers package init() -> auto-registration
}
π Lokstra Annotations Reference
@RouterService
Marks a struct as a router service - generates factory, remote proxy, and router registration.
Syntax:
// @RouterService name="service-name", prefix="/api", middlewares=["recovery", "logger"]
type MyService struct { ... }
Parameters:
name(required) - Service name for registrationprefix(optional) - URL prefix for all routes (default: ββ)middlewares(optional) - Array of middleware names (default: [])
Example:
// @RouterService name="user-service", prefix="/api/v1", middlewares=["auth", "logging"]
type UserServiceImpl struct { ... }
Generates:
UserServiceImplFactory()- Creates service instancesUserServiceImplRemote- HTTP proxy implementationRegisterUserServiceImpl()- Auto-registration function- Router configuration with all routes
@Inject
Auto-wires service dependencies - generates dependency injection code.
Syntax:
type MyService struct {
// @Inject "dependency-service-name"
DepField *service.Cached[InterfaceType]
}
Parameters:
- First string parameter - Service name to inject
Example:
type UserServiceImpl struct {
// @Inject "user-repository"
UserRepo *service.Cached[domain.UserRepository]
// @Inject "email-service"
EmailSvc *service.Cached[domain.EmailService]
}
Generates:
func UserServiceImplFactory(deps map[string]any, config map[string]any) any {
return &UserServiceImpl{
UserRepo: service.Cast[domain.UserRepository](deps["user-repository"]),
EmailSvc: service.Cast[domain.EmailService](deps["email-service"]),
}
}
// Auto-detected dependencies in registration
lokstra_registry.RegisterLazyService("user-service", "user-service-factory",
map[string]any{"depends-on": []string{"user-repository", "email-service"}})
@Route
Maps a method to an HTTP endpoint - generates route registration.
Syntax:
// @Route "METHOD /path/{param}"
func (s *MyService) MethodName(p *RequestType) (*ResponseType, error) { ... }
Supported HTTP Methods:
GET,POST,PUT,DELETE,PATCH,OPTIONS,HEAD
Path Parameters:
- Use
{paramName}for path variables - Maps to fields in request struct
Example:
// @Route "GET /users/{id}"
func (s *UserServiceImpl) GetByID(p *domain.GetUserRequest) (*domain.User, error) {
return s.UserRepo.MustGet().GetByID(p.ID)
}
// @Route "POST /users"
func (s *UserServiceImpl) Create(p *domain.CreateUserRequest) (*domain.User, error) {
return s.UserRepo.MustGet().Create(p)
}
// @Route "DELETE /users/{id}"
func (s *UserServiceImpl) Delete(p *domain.DeleteUserRequest) error {
return s.UserRepo.MustGet().Delete(p.ID)
}
Request Struct Binding:
type GetUserRequest struct {
ID int `path:"id" validate:"required"` // From URL path
}
type CreateUserRequest struct {
Name string `json:"name" validate:"required"` // From JSON body
Email string `json:"email" validate:"email"`
}
type ListUsersRequest struct {
Page int `query:"page"` // From query string
Size int `query:"size"`
}
Generated Route Map:
deploy.WithRouter(&deploy.ServiceTypeRouter{
PathPrefix: "/api",
CustomRoutes: map[string]string{
"GetByID": "GET /users/{id}",
"Create": "POST /users",
"Delete": "DELETE /users/{id}",
},
})
π§ How It Works
1. Bootstrap Phase
When you call lokstra.Bootstrap() in main():
func main() {
lokstra.Bootstrap() // π Magic starts here
// ... rest of your code
}
Bootstrap does:
- Detects run mode - Production, Development, or Debug
- Checks for code changes - Scans
.gofiles for modifications - Auto-generates code - Creates
zz_generated.lokstra.gofiles - Relaunches if needed - Restarts app to load new code
2. Annotation Processing
The annotation processor scans your code for:
// @RouterService name="user-service", prefix="/api"
type UserServiceImpl struct {
// @Inject "user-repository"
UserRepo *service.Cached[domain.UserRepository]
}
// @Route "GET /users/{id}"
func (s *UserServiceImpl) GetByID(...) { ... }
Processor extracts:
- Service metadata (name, prefix, middlewares)
- Dependencies from
@Injectannotations - Method signatures and return types
- Route definitions from
@Routeannotations
3. Code Generation
Generates zz_generated.lokstra.go:
// AUTO-GENERATED CODE - DO NOT EDIT
package application
func init() {
RegisterUserServiceImpl() // Auto-registers on import
}
// Factory function
func UserServiceImplFactory(deps map[string]any, config map[string]any) any {
return &UserServiceImpl{
UserRepo: service.Cast[domain.UserRepository](deps["user-repository"]),
}
}
// Remote HTTP proxy
type UserServiceImplRemote struct {
proxyService *proxy.Service
}
func (s *UserServiceImplRemote) GetByID(p *domain.GetUserRequest) (*domain.User, error) {
return proxy.CallWithData[*domain.User](s.proxyService, "GetByID", p)
}
// Registration function
func RegisterUserServiceImpl() {
lokstra_registry.RegisterServiceType("user-service-factory",
UserServiceImplFactory,
UserServiceImplRemoteFactory,
deploy.WithRouter(&deploy.ServiceTypeRouter{
PathPrefix: "/api",
Middlewares: []string{"recovery", "request-logger"},
CustomRoutes: map[string]string{
"GetByID": "GET /users/{id}",
// ... all routes
},
}),
)
lokstra_registry.RegisterLazyService("user-service",
"user-service-factory",
map[string]any{
"depends-on": []string{"user-repository"},
})
}
4. Auto-Registration via init()
When you import a module package:
import (
"github.com/.../modules/user"
)
func registerServiceTypes() {
user.Register() // Calls application.Register() -> triggers init()
}
The init() function in zz_generated.lokstra.go automatically runs and registers all services!
5. Cache System
Lokstra caches annotation processing results in zz_cache.lokstra.json:
{
"user_service.go": {
"hash": "abc123...",
"lastModified": "2025-11-11T10:30:00Z",
"annotations": [...]
}
}
Benefits:
- β Only regenerates changed files
- β Fast incremental builds
- β Preserves code for unchanged files
- β Minimal overhead in dev mode
6. Run Modes
| Mode | Detection | Behavior |
|---|---|---|
| Production | Compiled binary | Skip autogen, use existing generated code |
| Development | go run |
Auto-generate + relaunch with go run |
| Debug | Delve/VSCode debugger | Auto-generate + notify to restart debugger |
Development Mode:
$ go run .
[Lokstra] Environment detected: DEV
[Lokstra] Processing annotations...
[Lokstra] Code changed - relaunching...
# App restarts automatically with new code
Debug Mode (VSCode):
[Lokstra] Environment detected: DEBUG
[Lokstra] Processing annotations...
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β AUTOGEN COMPLETED - DEBUGGER RESTART REQUIRED β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β οΈ Code generation detected changes.
β οΈ Please STOP and RESTART your debugger (F5)
π Getting Started
Prerequisites
- Go 1.23 or higher
- VS Code with REST Client extension (for test.http)
Run the Application
# From project root
cd docs/00-introduction/examples/full-framework/01_enterprise_router_service
# Run in development mode (auto-reload)
go run .
On first run, youβll see:
[Lokstra] Environment detected: DEV
[Lokstra] Processing annotations...
Processing folder: .../modules/user/application
- Updated: 1 files
- Generated: zz_generated.lokstra.go
Processing folder: .../modules/order/application
- Updated: 1 files
- Generated: zz_generated.lokstra.go
[Lokstra] Relaunching with go run...
βββββββββββββββββββββββββββββββββββββββββββββββββ
β LOKSTRA ENTERPRISE ROUTER SERVICE β
β Annotation-Driven Auto-Generated Routers β
βββββββββββββββββββββββββββββββββββββββββββββββββ
Server starting on :3000
Server will start on http://localhost:3000
Test the APIs
Open test.http in VS Code and click βSend Requestβ above each API call.
User APIs:
### Get all users
GET http://localhost:3000/api/users
### Get user by ID
GET http://localhost:3000/api/users/1
### Create new user
POST http://localhost:3000/api/users
Content-Type: application/json
{
"name": "Alice Johnson",
"email": "alice@example.com",
"role_id": 2
}
### Update user
PUT http://localhost:3000/api/users/1
Content-Type: application/json
{
"name": "Alice Updated",
"email": "alice.updated@example.com",
"role_id": 2
}
### Suspend user
POST http://localhost:3000/api/users/1/suspend
### Delete user
DELETE http://localhost:3000/api/users/1
β Adding New Modules
Step 1: Create Module Structure
mkdir -p modules/product/{domain,application,infrastructure/repository}
Step 2: Define Domain Layer
modules/product/domain/entity.go:
package domain
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
modules/product/domain/service.go:
package domain
type ProductService interface {
GetByID(p *GetProductRequest) (*Product, error)
List(p *ListProductsRequest) ([]*Product, error)
Create(p *CreateProductRequest) (*Product, error)
}
type ProductRepository interface {
GetByID(id int) (*Product, error)
List() ([]*Product, error)
Create(p *Product) (*Product, error)
}
modules/product/domain/dto.go:
package domain
type GetProductRequest struct {
ID int `path:"id" validate:"required"`
}
type ListProductsRequest struct {
Category string `query:"category"`
}
type CreateProductRequest struct {
Name string `json:"name" validate:"required"`
Price float64 `json:"price" validate:"required,gt=0"`
}
Step 3: Implement Application Layer with Annotations
modules/product/application/product_service.go:
package application
import (
"github.com/primadi/lokstra/core/service"
"github.com/primadi/lokstra/.../modules/product/domain"
)
// @RouterService name="product-service", prefix="/api", middlewares=["recovery", "request-logger"]
type ProductServiceImpl struct {
// @Inject "product-repository"
ProductRepo *service.Cached[domain.ProductRepository]
}
// Ensure implementation
var _ domain.ProductService = (*ProductServiceImpl)(nil)
// @Route "GET /products/{id}"
func (s *ProductServiceImpl) GetByID(p *domain.GetProductRequest) (*domain.Product, error) {
return s.ProductRepo.MustGet().GetByID(p.ID)
}
// @Route "GET /products"
func (s *ProductServiceImpl) List(p *domain.ListProductsRequest) ([]*domain.Product, error) {
return s.ProductRepo.MustGet().List()
}
// @Route "POST /products"
func (s *ProductServiceImpl) Create(p *domain.CreateProductRequest) (*domain.Product, error) {
product := &domain.Product{
Name: p.Name,
Price: p.Price,
}
return s.ProductRepo.MustGet().Create(product)
}
func Register() {
// Empty function to trigger package load
}
Step 4: Implement Infrastructure Layer
modules/product/infrastructure/repository/product_repository.go:
package repository
import "github.com/primadi/lokstra/.../modules/product/domain"
type ProductRepositoryImpl struct {
products map[int]*domain.Product
nextID int
}
func (r *ProductRepositoryImpl) GetByID(id int) (*domain.Product, error) {
if p, exists := r.products[id]; exists {
return p, nil
}
return nil, fmt.Errorf("product not found")
}
func (r *ProductRepositoryImpl) List() ([]*domain.Product, error) {
result := make([]*domain.Product, 0, len(r.products))
for _, p := range r.products {
result = append(result, p)
}
return result, nil
}
func (r *ProductRepositoryImpl) Create(p *domain.Product) (*domain.Product, error) {
r.nextID++
p.ID = r.nextID
r.products[p.ID] = p
return p, nil
}
func ProductRepositoryFactory(deps map[string]any, config map[string]any) any {
return &ProductRepositoryImpl{
products: make(map[int]*domain.Product),
nextID: 0,
}
}
Step 5: Create Module Registration
modules/product/register.go:
package product
import (
"github.com/primadi/lokstra/lokstra_registry"
"github.com/primadi/lokstra/.../modules/product/application"
"github.com/primadi/lokstra/.../modules/product/infrastructure/repository"
)
func Register() {
// Register repository
lokstra_registry.RegisterServiceType("product-repository-factory",
repository.ProductRepositoryFactory, nil)
lokstra_registry.RegisterLazyService("product-repository",
"product-repository-factory", nil)
// Trigger auto-registration from annotations
application.Register()
}
Step 6: Register Module in Main
register.go:
import (
"github.com/primadi/lokstra/.../modules/user"
"github.com/primadi/lokstra/.../modules/order"
"github.com/primadi/lokstra/.../modules/product" // Add this
)
func registerServiceTypes() {
user.Register()
order.Register()
product.Register() // Add this
}
Step 7: Create Config (Optional)
config/product.yaml:
deployments:
- name: api-server
type: server
port: 3000
services:
- name: product-repository
factory: product-repository-factory
- name: product-service
factory: product-service-factory
dependencies:
product-repository: product-repository
Step 8: Run and Test!
go run .
Bootstrap will automatically:
- β
Detect new
product_service.gowith annotations - β
Generate
zz_generated.lokstra.goin product/application/ - β Register all routes, dependencies, and factories
- β Relaunch the app with new code
Your product API is now live!
GET /api/products
POST /api/products
GET /api/products/{id}
No manual registration needed! π
π Code Generation
Generated Files
Every folder with @RouterService annotations gets:
zz_generated.lokstra.go - Generated Go code
// AUTO-GENERATED CODE - DO NOT EDIT
package application
func init() { RegisterUserServiceImpl() }
func UserServiceImplFactory(...) { ... }
type UserServiceImplRemote struct { ... }
func RegisterUserServiceImpl() { ... }
zz_cache.lokstra.json - Cache metadata (gitignore this!)
{
"user_service.go": {
"hash": "abc123...",
"lastModified": "2025-11-11T10:30:00Z"
}
}
When Code is Regenerated
Code regenerates automatically when:
| Trigger | Action |
|---|---|
| File modified | Detected by hash + timestamp comparison |
| Annotation changed | Service name, routes, dependencies updated |
| Method added/removed | New routes auto-registered |
| Dependency added | Auto-wired in factory |
Cache Behavior
First run:
Processing folder: modules/user/application
- Updated: 1 files (user_service.go)
- Generated: zz_generated.lokstra.go
No changes:
Processing folder: modules/user/application
- Skipped: 1 files (no changes)
File modified:
Processing folder: modules/user/application
- Updated: 1 files (user_service.go)
- Regenerated: zz_generated.lokstra.go
Manual Regeneration
Force regeneration (delete cache):
find . -name "zz_cache.lokstra.json" -delete
go run .
π’ Deployment Strategies
1. Monolith (Current Setup)
All modules in one server
# config/monolith.yaml
deployments:
- name: api-server
type: server
port: 3000
services:
- name: user-service
factory: user-service-factory
- name: order-service
factory: order-service-factory
go run . -config=config/monolith.yaml
Pros: Simple, low latency, easy development
Cons: All modules must scale together
2. Microservices
Each module as separate service
config/user-service.yaml:
deployments:
- name: user-service
type: server
port: 3001
services:
- name: user-service
factory: user-service-factory
- name: user-repository
factory: user-repository-factory
config/order-service.yaml:
deployments:
- name: order-service
type: server
port: 3002
services:
- name: order-service
factory: order-service-factory
- name: order-repository
factory: order-repository-factory
Run each as separate process:
# Terminal 1
go run . -config=config/user-service.yaml
# Terminal 2
go run . -config=config/order-service.yaml
Pros: Independent scaling, deployment
Cons: Network latency, distributed complexity
Same code, different deployment! β¨
π Comparison with Manual Registration
Without Annotations (Manual)
70+ lines of boilerplate per service:
// user_service.go
type UserServiceImpl struct {
UserRepo *service.Cached[domain.UserRepository]
}
func (s *UserServiceImpl) GetByID(p *domain.GetUserRequest) (*domain.User, error) {
return s.UserRepo.MustGet().GetByID(p.ID)
}
// ... all methods ...
// Manual factory
func UserServiceFactory(deps map[string]any, config map[string]any) any {
return &UserServiceImpl{
UserRepo: service.Cast[domain.UserRepository](deps["user-repository"]),
}
}
// Manual remote proxy
type UserServiceRemote struct {
proxyService *proxy.Service
}
func (s *UserServiceRemote) GetByID(p *domain.GetUserRequest) (*domain.User, error) {
return proxy.CallWithData[*domain.User](s.proxyService, "GetByID", p)
}
// ... all proxy methods ...
func UserServiceRemoteFactory(deps, config map[string]any) any {
proxyService := config["remote"].(*proxy.Service)
return &UserServiceRemote{proxyService: proxyService}
}
// Manual registration
func init() {
lokstra_registry.RegisterServiceType("user-service-factory",
UserServiceFactory,
UserServiceRemoteFactory,
deploy.WithRouter(&deploy.ServiceTypeRouter{
PathPrefix: "/api",
CustomRoutes: map[string]string{
"GetByID": "GET /users/{id}",
"List": "GET /users",
"Create": "POST /users",
// ... manual route mapping
},
}),
)
lokstra_registry.RegisterLazyService("user-service",
"user-service-factory",
map[string]any{
"depends-on": []string{"user-repository"},
})
}
Problems:
- β 70+ lines of boilerplate
- β Error-prone manual route mapping
- β Easy to forget dependency registration
- β Remote proxy must match interface exactly
- β Every method change requires 3 updates
With Annotations (Auto-Generated)
12 lines of business logic:
// @RouterService name="user-service", prefix="/api"
type UserServiceImpl struct {
// @Inject "user-repository"
UserRepo *service.Cached[domain.UserRepository]
}
// @Route "GET /users/{id}"
func (s *UserServiceImpl) GetByID(p *domain.GetUserRequest) (*domain.User, error) {
return s.UserRepo.MustGet().GetByID(p.ID)
}
// @Route "GET /users"
func (s *UserServiceImpl) List(p *domain.ListUsersRequest) ([]*domain.User, error) {
return s.UserRepo.MustGet().List()
}
// @Route "POST /users"
func (s *UserServiceImpl) Create(p *domain.CreateUserRequest) (*domain.User, error) {
// ... business logic
}
func Register() {} // Trigger package load
Benefits:
- β 12 lines vs 70+ lines
- β No manual factory/proxy code
- β Routes auto-detected from signatures
- β Dependencies auto-wired
- β Type-safe code generation
- β Add method β auto-registered
Comparison Table
| Aspect | Manual | With Annotations |
|---|---|---|
| Lines of Code | 70+ per service | 12 per service |
| Factory Function | Manual | Auto-generated |
| Remote Proxy | Manual implementation | Auto-generated |
| Route Registration | Manual mapping | Auto-detected |
| Dependency Wiring | Manual | Auto-detected |
| Type Safety | Manual sync required | Compiler-enforced |
| Refactoring | Update 3+ places | Update 1 place |
| Error Prone | High | Low |
| Development Speed | Slow | Fast |
83% less code with annotations! π
π Key Concepts
Annotation-Driven Development
Annotations are declarative metadata that describe what you want, not how to implement it.
// DECLARATIVE: What you want
// @RouterService name="user-service", prefix="/api"
// @Route "GET /users/{id}"
// vs.
// IMPERATIVE: How to implement (manual)
router.GET("/api/users/:id", handler)
lokstra_registry.RegisterService(...)
Code Generation vs. Reflection
Lokstra uses code generation, not runtime reflection:
| Approach | When | Performance | Type Safety | Debuggability |
|---|---|---|---|---|
| Reflection | Runtime | Slower | Weak | Hard |
| Code Generation | Build time | Fast | Strong | Easy |
Benefits:
- β Zero runtime overhead
- β Full type checking at compile time
- β Generated code is readable and debuggable
- β No βmagicβ - you can see exactly whatβs generated
Convention over Configuration
Lokstra follows smart defaults with explicit overrides:
// Default convention (REST):
// Method name β Route
// GetByID β GET /{resource}/{id}
// List β GET /{resource}
// Create β POST /{resource}
// Override when needed:
// @Route "POST /users/{id}/special-action"
func (s *UserService) SpecialAction(...) { ... }
π Best Practices
1. Keep Annotations Simple
// β
Good - clear and concise
// @RouterService name="user-service", prefix="/api"
type UserServiceImpl struct { ... }
// β Bad - too complex
// @RouterService name="user-service", prefix="/api/v1/internal/services", middlewares=["auth", "rbac", "logging", "metrics", "tracing"]
2. Use Descriptive Service Names
// β
Good - follows naming convention
// @RouterService name="user-service"
// β Bad - inconsistent naming
// @RouterService name="usrSvc"
3. Group Related Routes
// β
Good - consistent prefix
// @RouterService name="user-service", prefix="/api/v1/users"
// @Route "GET /{id}"
func GetByID(...) { ... }
// @Route "POST /"
func Create(...) { ... }
4. Document Business Logic
// β
Good - document why, not what
// @Route "POST /users/{id}/suspend"
// Suspends user account - prevents login but preserves data
func (s *UserServiceImpl) Suspend(...) { ... }
// β Bad - annotations already show what
// @Route "GET /users/{id}"
// Gets a user by ID
func (s *UserServiceImpl) GetByID(...) { ... }
5. Use Interface Assertion
// β
Good - ensures implementation matches interface
var _ domain.UserService = (*UserServiceImpl)(nil)
// Compiler error if interface doesn't match!
β οΈ Important: Code Generation Before Build
The Problem
Auto-generation only happens during go run, not during go build!
This means:
- β Running
go builddirectly will fail if generated files donβt exist - β Running
go buildafter code changes will use old generated code - β
Must run
go run .at least once before building - β
Must run
go run .after every annotation change
Why This Happens
| Command | Run Mode | Code Generation |
|---|---|---|
go run . |
Development | β Auto-generates |
go build |
Production | β Skips generation |
./compiled-binary |
Production | β Skips generation |
Bootstrap logic:
func Bootstrap() {
Mode = detectRunMode()
if Mode == RunModeProd {
return // Skip autogen in production!
}
// Only dev/debug modes auto-generate
runAutoGen()
}
Solutions to Avoid Forgetting
Solution 1: Use Build Scripts β RECOMMENDED
Provided build scripts handle everything automatically!
Windows PowerShell:
.\build.ps1 # Generates + Builds for Windows
.\build.ps1 linux # Generates + Builds for Linux
.\build.ps1 darwin # Generates + Builds for macOS
Windows CMD:
build.bat # Generates + Builds for Windows
build.bat linux # Generates + Builds for Linux
build.bat darwin # Generates + Builds for macOS
Linux/Mac:
./build.sh # Generates + Builds for current platform
./build.sh linux # Generates + Builds for Linux
./build.sh windows # Generates + Builds for Windows
./build.sh darwin # Generates + Builds for macOS
What the scripts do:
- β
Force code generation (
go run . --generate-only) - β
Tidy dependencies (
go mod tidy) - β Build binary with platform-specific name
- β Cross-platform support (build for any OS from any OS)
You never have to remember! Just run the script. π
Solution 2: Always Use go run in Development
Recommended workflow:
# During development - ALWAYS use go run
go run .
# Code autogenerates, then runs
# Edit code β Ctrl+C β go run . again
Never use go build during development!
Solution 3: Manual Generation Flag
If you must build manually:
# Step 1: Force generate (this is the key!)
go run . --generate-only
# Step 2: Build normally
go build -o app.exe .
The --generate-only flag:
- β Forces rebuild of all generated code
- β Deletes cache files automatically
- β Exits after generation (doesnβt run the app)
- β Perfect for build scripts
Solution 4: CI/CD Pipeline
GitHub Actions (.github/workflows/build.yml):
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
# Use --generate-only flag
- name: Generate Code
run: go run . --generate-only
- name: Tidy Dependencies
run: go mod tidy
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
# Build for multiple platforms
- name: Build Releases
run: |
GOOS=linux GOARCH=amd64 go build -o dist/app-linux .
GOOS=windows GOARCH=amd64 go build -o dist/app-windows.exe .
GOOS=darwin GOARCH=amd64 go build -o dist/app-darwin .
Recommended Workflow
For Development:
# 1. Edit your service files (add/change @Route, @Inject, etc.)
# 2. Run with auto-generation
go run .
# 3. Test your changes
curl http://localhost:3000/api/users
# 4. Repeat steps 1-3
For Production Build:
Option A: Using Build Scripts (Recommended) β
# Windows (PowerShell)
.\build.ps1 # Build for Windows
.\build.ps1 linux # Cross-compile for Linux
.\build.ps1 darwin # Cross-compile for macOS
# Windows (CMD)
build.bat # Build for Windows
build.bat linux # Cross-compile for Linux
build.bat darwin # Cross-compile for macOS
# Linux/Mac
./build.sh # Build for current platform
./build.sh linux # Build for Linux
./build.sh windows # Cross-compile for Windows
./build.sh darwin # Build for macOS
What build scripts do:
- Run
go run . --generate-only(force code generation) - Run
go mod tidy(ensure dependencies) - Run
go build(compile binary) - Create platform-specific binary:
- Windows:
app-windows.exe - Linux:
app-linux - macOS:
app-darwin
- Windows:
Option B: Manual (Not Recommended)
# Step 1: Force generate code
go run . --generate-only
# Step 2: Tidy dependencies (optional but recommended)
go mod tidy
# Step 3: Build
go build -o app.exe .
# For cross-platform:
GOOS=linux GOARCH=amd64 go build -o app-linux .
GOOS=windows GOARCH=amd64 go build -o app-windows.exe .
GOOS=darwin GOARCH=amd64 go build -o app-darwin .
For CI/CD:
# GitHub Actions example
- name: Generate Code
run: go run . --generate-only
- name: Tidy Dependencies
run: go mod tidy
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
Visual Workflow Reminder
βββββββββββββββββββββββββββββββββββββββββββββββ
β Development Mode β
β β
β Edit Code β go run . β Test β Repeat β
β β β
β ββ Auto-generates here β
βββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Production Build (Recommended) β
β β
β ./build.sh β Deploy binary β
β β β
β ββ Generates + Builds in one step! β
βββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Production Build (Manual) β
β β
β go run . --generate-only β go build β
β β β
β ββ Force generation flag β
βββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββ
β β WRONG - Will Fail or Use Old Code! β
β β
β Edit Code β go build β ERROR/STALE β
β β β
β ββ No generation = problems! β
βββββββββββββββββββββββββββββββββββββββββββββββ
Available Build Scripts
The project includes cross-platform build scripts:
| Script | Platform | Description |
|---|---|---|
build.ps1 |
Windows (PowerShell) | Recommended for Windows users |
build.bat |
Windows (CMD) | For legacy Windows environments |
build.sh |
Linux/macOS | Unix-based systems |
All scripts support:
- Current platform build
- Cross-compilation (Linux, Windows, macOS)
- Automatic code generation
- Dependency tidying
Example usage:
# Windows PowerShell
.\build.ps1 # Build for Windows
.\build.ps1 linux # Cross-compile for Linux
.\build.ps1 darwin # Cross-compile for macOS
# Windows CMD
build.bat windows # Build for Windows
build.bat linux # Cross-compile for Linux
# Linux/Mac
./build.sh # Build for current platform
./build.sh windows # Cross-compile for Windows
The --generate-only Flag
New in Lokstra! Force code generation without running the app.
Usage:
go run . --generate-only
What it does:
- β
Deletes all
zz_cache.lokstra.jsonfiles - β Forces rebuild of ALL generated code
- β Exits after generation (doesnβt run the app)
- β Perfect for build scripts and CI/CD
When to use:
- Before building production binaries
- In CI/CD pipelines
- After major code refactoring
- When cache seems corrupted
Example in build script:
# Force generation before build
go run . --generate-only
# Now safe to build
go build -o app .
π§ Troubleshooting
Code Not Regenerating
Problem: Changed annotations but code not updating
Solutions:
# 1. Delete cache and rerun
find . -name "zz_cache.lokstra.json" -delete
go run .
# 2. Check run mode
[Lokstra] Environment detected: PROD # Won't autogen in prod!
# 3. Force dev mode
export LOKSTRA_MODE=dev
go run .
Debugger Not Stopping at Breakpoints
Problem: After code generation, debugger doesnβt work
Solution:
[Lokstra] Environment detected: DEBUG
[Lokstra] Code changed - please RESTART debugger
β Press Ctrl+C
β Press F5 to restart debugger
Import Cycle Errors
Problem: Circular imports between modules
Solution:
// β Bad - circular dependency
modules/user β modules/order
modules/order β modules/user
// β
Good - shared domain
modules/user β modules/shared
modules/order β modules/shared
Annotation Not Recognized
Problem: Annotation not being processed
Checklist:
- β
Correct syntax:
// @RouterService(with space after//) - β
Correct parameter format:
name="value" - β File saved before running
- β
Not in test file (
_test.go)
π Learn More
- Lokstra Documentation
- Annotation Processing Deep Dive
- Service Layer Guide
- Dependency Injection Patterns
π License
This template is part of the Lokstra framework. See LICENSE file in project root.