Quick Start Guide

Build your first Lokstra API in 5 minutes


πŸ“‹ Prerequisites

Before starting, make sure you have:

# Go 1.21 or higher
go version
# go version go1.21.0 or later

πŸš€ Step 1: Create Project

# Create project directory
mkdir my-first-api
cd my-first-api

# Initialize Go module
go mod init my-first-api

# Install Lokstra
go get github.com/primadi/lokstra@latest

πŸ“ Step 2: Hello World (Minimal)

Create main.go:

package main

import (
    "github.com/primadi/lokstra"
    "time"
)

func main() {
    // 1. Create router
    r := lokstra.NewRouter("api")
    
    // 2. Add route
    r.GET("/ping", func() string {
        return "pong"
    })
    
    // 3. Create app and run
    app := lokstra.NewApp("demo", ":3000", r)
    app.Run(30 * time.Second)
}

Run it:

go run main.go
# πŸš€ Server starting on http://localhost:3000

Test it:

curl http://localhost:3000/ping
# "pong"

βœ… Congratulations! You’ve built your first Lokstra API!


🎯 Step 3: Add More Routes

Let’s build a simple User API:

package main

import (
    "fmt"
    "github.com/primadi/lokstra"
    "time"
)

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

// In-memory storage
var users = []User{
    {ID: 1, Name: "Alice", Email: "alice@example.com"},
    {ID: 2, Name: "Bob", Email: "bob@example.com"},
}
var nextID = 3

func main() {
    r := lokstra.NewRouter("api")
    
    // GET /users - List all users
    r.GET("/users", func() ([]User, error) {
        return users, nil
    })
    
    // GET /users/{id} - Get one user
    r.GET("/users/{id}", getUser)
    
    // POST /users - Create user
    r.POST("/users", createUser)
    
    // PUT /users/{id} - Update user
    r.PUT("/users/{id}", updateUser)
    
    // DELETE /users/{id} - Delete user
    r.DELETE("/users/{id}", deleteUser)
    
    app := lokstra.NewApp("user-api", ":3000", r)
    fmt.Println("πŸš€ User API running on http://localhost:3000")
    app.Run(30 * time.Second)
}

// Request/Response types
type GetUserRequest struct {
    ID int `path:"id"`
}

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

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

type DeleteUserRequest struct {
    ID int `path:"id"`
}

// Handlers
func getUser(req *GetUserRequest) (*User, error) {
    for _, u := range users {
        if u.ID == req.ID {
            return &u, nil
        }
    }
    return nil, fmt.Errorf("user not found")
}

func createUser(req *CreateUserRequest) (*User, error) {
    user := User{
        ID:    nextID,
        Name:  req.Name,
        Email: req.Email,
    }
    nextID++
    users = append(users, user)
    return &user, nil
}

func updateUser(req *UpdateUserRequest) (*User, error) {
    for i, u := range users {
        if u.ID == req.ID {
            if req.Name != "" {
                users[i].Name = req.Name
            }
            if req.Email != "" {
                users[i].Email = req.Email
            }
            return &users[i], nil
        }
    }
    return nil, fmt.Errorf("user not found")
}

func deleteUser(req *DeleteUserRequest) error {
    for i, u := range users {
        if u.ID == req.ID {
            users = append(users[:i], users[i+1:]...)
            return nil
        }
    }
    return fmt.Errorf("user not found")
}

πŸ§ͺ Step 4: Test Your API

List all users:

curl http://localhost:3000/users
[
  {"id":1,"name":"Alice","email":"alice@example.com"},
  {"id":2,"name":"Bob","email":"bob@example.com"}
]

Get one user:

curl http://localhost:3000/users/1
{"id":1,"name":"Alice","email":"alice@example.com"}

Create new user:

curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Charlie","email":"charlie@example.com"}'
{"id":3,"name":"Charlie","email":"charlie@example.com"}

Update user:

curl -X PUT http://localhost:3000/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice Smith"}'
{"id":1,"name":"Alice Smith","email":"alice@example.com"}

Delete user:

curl -X DELETE http://localhost:3000/users/2

πŸ“š What Just Happened?

Let’s break down the magic:

1. Flexible Handler Signatures

// Simple return
r.GET("/users", func() ([]User, error) {
    return users, nil
})

// With request binding
r.POST("/users", func(req *CreateUserRequest) (*User, error) {
    return createUser(req)
})

Lokstra automatically:

2. Auto Request Binding

type GetUserRequest struct {
    ID int `path:"id"`  // From URL path
}

type CreateUserRequest struct {
    Name  string `json:"name"`   // From JSON body
    Email string `json:"email"`
}

Tags:

3. Auto JSON Response

func getUser(req *GetUserRequest) (*User, error) {
    return &user, nil  // Automatically becomes JSON
}

Lokstra converts:


πŸ—οΈ Step 6: Add Services & Dependency Injection

Let’s organize code with service layer and lazy loading:

package main

import (
    "fmt"
    "github.com/primadi/lokstra"
    "github.com/primadi/lokstra/core/service"
    "github.com/primadi/lokstra/lokstra_registry"
    "time"
)

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

// Repository (data layer)
type UserRepository struct {
    users map[int]*User
}

func NewUserRepository() *UserRepository {
    return &UserRepository{
        users: map[int]*User{
            1: {ID: 1, Name: "Alice", Email: "alice@example.com"},
            2: {ID: 2, Name: "Bob", Email: "bob@example.com"},
        },
    }
}

func (r *UserRepository) FindAll() ([]*User, error) {
    var result []*User
    for _, u := range r.users {
        result = append(result, u)
    }
    return result, nil
}

// Service (business logic layer)
type UserService struct {
    Repo *service.Cached[*UserRepository]
}

func UserServiceFactory(deps map[string]any, config map[string]any) any {
    return &UserService{
        Repo: service.Cast[*UserRepository](deps["user-repo"]),
    }
}

func (s *UserService) GetAll() ([]*User, error) {
    return s.Repo.MustGet().FindAll()
}

func main() {
    // 1. Register repository
    lokstra_registry.RegisterServiceType("user-repo-factory",
        NewUserRepository, nil)
    
    lokstra_registry.RegisterLazyService("user-repo", 
        "user-repo-factory", nil)
    
    // 2. Register service with dependency
    lokstra_registry.RegisterServiceFactory("user-service-factory", 
        UserServiceFactory)
    
    lokstra_registry.RegisterLazyService("user-service",
        "user-service-factory",
        map[string]any{
            "depends-on": []string{"user-repo"},
        })
    
    // 3. Use service in handler with lazy loading
    var userService = service.LazyLoad[*UserService]("user-service")
    
    r := lokstra.NewRouter("api")
    r.GET("/users", func() ([]*User, error) {
        return userService.MustGet().GetAll()
    })
    
    app := lokstra.NewApp("user-api", ":3000", r)
    fmt.Println("πŸš€ User API with Services running on http://localhost:3000")
    app.Run(30 * time.Second)
}

What’s happening:

  1. Repository - Data access layer
  2. Service - Business logic layer with lazy dependencies
  3. Registry - Service registration with factory pattern
  4. Lazy Loading - service.LazyLoad[T] creates cached reference
  5. MustGet() - Resolves service once, panics if not found

Test it:

curl http://localhost:3000/users

Benefits:


βš™οΈ Step 7: Add YAML Configuration

Scale up with configuration file:

config.yaml:

service-definitions:
  user-repo:
    type: user-repo-factory
  
  user-service:
    type: user-service-factory
    depends-on: [user-repo]

deployments:
  app:
    servers:
      api-server:
        base-url: "http://localhost"
        addr: ":3000"
        published-services:
          - user-service

main.go:

package main

import (
    "flag"
    "time"
    "github.com/primadi/lokstra/lokstra_registry"
)

var server = flag.String("server", "app.api-server", "Server to run")

func main() {
    flag.Parse()
    
    // Register factories (same as before)
    lokstra_registry.RegisterServiceType("user-repo-factory",
        NewUserRepository, nil)
    
    lokstra_registry.RegisterServiceFactory("user-service-factory",
        UserServiceFactory)
    
    // Load config and auto-build services + routers
    lokstra_registry.LoadAndBuild([]string{"config.yaml"})
    
    // Run server
    lokstra_registry.RunServer(*server, 30*time.Second)
}

Run it:

go run main.go -server "app.api-server"

What changed:


πŸš€ Step 8: Multi-Deployment (Monolith β†’ Microservices)

Same code, different deployments!

config.yaml:

service-definitions:
  user-repo:
    type: user-repo-factory
  
  user-service:
    type: user-service-factory
    depends-on: [user-repo]
  
  order-service:
    type: order-service-factory
    depends-on: [user-service]  # Can be local OR remote!

deployments:
  # Deployment 1: Monolith (all in one server)
  monolith:
    servers:
      api-server:
        addr: ":3000"
        published-services:
          - user-service
          - order-service
  
  # Deployment 2: Microservices (separate servers)
  microservices:
    servers:
      user-server:
        addr: ":3001"
        published-services: [user-service]
      
      order-server:
        addr: ":3002"
        published-services: [order-service]
        # user-service auto-detected as remote!

Run monolith:

go run main.go -server "monolith.api-server"
# All services on :3000

Run microservices:

# Terminal 1
go run main.go -server "microservices.user-server"

# Terminal 2
go run main.go -server "microservices.order-server"
# Automatically makes HTTP calls to user-server!

Benefits:


🎨 Step 5: Add Middleware

Let’s add logging:

import (
    "github.com/primadi/lokstra"
    "github.com/primadi/lokstra/middleware/request_logger"
)

func main() {
    r := lokstra.NewRouter("api")
    
    // Add logging middleware
    r.Use(request_logger.Middleware(nil))
    
    // Your routes...
    r.GET("/users", func() ([]User, error) {
        return users, nil
    })
    
    app := lokstra.NewApp("user-api", ":3000", r)
    app.Run(30 * time.Second)
}

Now every request is logged:

[INFO] GET /users 200 OK (5ms)
[INFO] POST /users 200 OK (12ms)
[INFO] GET /users/1 200 OK (3ms)

πŸ”§ Optional: Add CORS

For frontend access:

import (
    "github.com/primadi/lokstra/middleware/cors"
)

func main() {
    r := lokstra.NewRouter("api")
    
    // Add CORS
    corsConfig := map[string]any{
        "allow_origins": []string{"*"},
        "allow_methods": []string{"GET", "POST", "PUT", "DELETE"},
    }
    r.Use(
        request_logger.Middleware(nil),
        cors.Middleware(corsConfig),
    )
    
    // Routes...
    
    app := lokstra.NewApp("user-api", ":3000", r)
    app.Run(30 * time.Second)
}

πŸ“Š Complete Example

Here’s the full working code:

Click to expand complete code ```go package main import ( "fmt" "github.com/primadi/lokstra" "github.com/primadi/lokstra/middleware/request_logger" "github.com/primadi/lokstra/middleware/cors" "time" ) type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` } var users = []User{ {ID: 1, Name: "Alice", Email: "alice@example.com"}, {ID: 2, Name: "Bob", Email: "bob@example.com"}, } var nextID = 3 func main() { r := lokstra.NewRouter("api") // Middleware corsConfig := map[string]any{ "allow_origins": []string{"*"}, "allow_methods": []string{"GET", "POST", "PUT", "DELETE"}, } r.Use( request_logger.Middleware(nil), cors.Middleware(corsConfig), ) // Routes r.GET("/users", func() ([]User, error) { return users, nil }) r.GET("/users/{id}", getUser) r.POST("/users", createUser) r.PUT("/users/{id}", updateUser) r.DELETE("/users/{id}", deleteUser) // Start app := lokstra.NewApp("user-api", ":3000", r) fmt.Println("πŸš€ User API running on http://localhost:3000") fmt.Println("πŸ“– Try: curl http://localhost:3000/users") app.Run(30 * time.Second) } // Request types type GetUserRequest struct { ID int `path:"id"` } type CreateUserRequest struct { Name string `json:"name" validate:"required"` Email string `json:"email" validate:"required,email"` } type UpdateUserRequest struct { ID int `path:"id"` Name string `json:"name"` Email string `json:"email"` } type DeleteUserRequest struct { ID int `path:"id"` } // Handlers func getUser(req *GetUserRequest) (*User, error) { for _, u := range users { if u.ID == req.ID { return &u, nil } } return nil, fmt.Errorf("user not found") } func createUser(req *CreateUserRequest) (*User, error) { user := User{ ID: nextID, Name: req.Name, Email: req.Email, } nextID++ users = append(users, user) return &user, nil } func updateUser(req *UpdateUserRequest) (*User, error) { for i, u := range users { if u.ID == req.ID { if req.Name != "" { users[i].Name = req.Name } if req.Email != "" { users[i].Email = req.Email } return &users[i], nil } } return nil, fmt.Errorf("user not found") } func deleteUser(req *DeleteUserRequest) error { for i, u := range users { if u.ID == req.ID { users = append(users[:i], users[i+1:]...) return nil } } return fmt.Errorf("user not found") } ```

🎯 What You’ve Learned

In this guide, you’ve mastered:

Basic Concepts (Steps 1-4):

Middleware (Step 5):

Advanced Features (Steps 6-8):

You now have a production-ready foundation! πŸŽ‰


πŸš€ Next Steps

Want to Learn More?

Systematic Learning (Recommended): πŸ‘‰ Examples - 7 progressive examples (6-8 hours)

Specific Topics:

Architecture Deep Dive: πŸ‘‰ Architecture Guide - How Lokstra works under the hood πŸ‘‰ Why Lokstra - Philosophy and design decisions


πŸ’‘ Tips for Beginners

1. Start Simple

// Begin with this
r.GET("/users", func() ([]User, error) {
    return getUsers()
})

// Add complexity as needed
r.GET("/users", func(ctx *request.Context, req *GetUsersRequest) (*response.Response, error) {
    // Full control
})

2. Use Struct Tags

type Request struct {
    ID    int    `path:"id"`       // From URL
    Page  int    `query:"page"`    // From query string
    Name  string `json:"name"`     // From body
    Token string `header:"X-Token"` // From header
}

3. Lazy Loading for Services

// Package-level (cached, recommended)
var userService = service.LazyLoad[*UserService]("users")

func handler() {
    users, err := userService.MustGet().GetAll()
    // MustGet() panics with clear error if service not found
}

4. Print Routes for Debugging

r := lokstra.NewRouter("api")
// ... add routes ...

r.PrintRoutes()  // See all registered routes

Output:

[api] GET /users -> api.GET[users]
[api] POST /users -> api.POST[users]
[api] GET /users/{id} -> api.GET[users_id]

πŸ†˜ Having Issues?

Common Problems:

β€œCannot find package”

go get github.com/primadi/lokstra@latest
go mod tidy

β€œPort already in use”

// Change port
app := lokstra.NewApp("demo", ":3001", r)  // Use 3001

β€œHandler signature error” Check supported forms in Router Guide


πŸ“š Resources


Happy coding! πŸš€

Next: Learn Router Guide or Understand Architecture