Router - Essential Guide
HTTP routing made flexible and intuitive
Time: 45 minutes (with examples) β’ Level: Beginner
π What Youβll Learn
- β Create routers and register routes
- β Write handlers in 5 essential forms (out of 29 total!)
- β Handle path parameters and query strings
- β Organize routes with groups
- β Master 3 response patterns (Manual, Generic, Opinionated)
- β Apply middleware to routes
π― What is a Router?
A Router is Lokstraβs HTTP request matcher. It:
- Matches incoming requests to handlers
- Extracts path parameters
- Applies middleware
- Invokes your handler function
Key Insight: Router implements http.Handler, so you can use it directly:
r := lokstra.NewRouter("api")
r.GET("/ping", func() string { return "pong" })
// Use directly with Go's http package!
http.ListenAndServe(":8080", r)
π Quick Start (2 Minutes)
package main
import (
"github.com/primadi/lokstra"
"time"
)
func main() {
// 1. Create router
r := lokstra.NewRouter("api")
// 2. Register routes
r.GET("/ping", func() string {
return "pong"
})
r.GET("/users", func() []string {
return []string{"Alice", "Bob"}
})
// 3. Create app and run
app := lokstra.NewApp("demo", ":3000", r)
if err := app.Run(30 * time.Second); err != nil {
fmt.Println("Error starting server:", err)
}
}
Test it:
curl http://localhost:3000/ping # β "pong"
curl http://localhost:3000/users # β ["Alice","Bob"]
π Basic Concepts
1. Creating a Router
// Simple router
r := lokstra.NewRouter("my-api")
// Router with specific engine (advanced)
r := lokstra.NewRouterWithEngine("my-api", "httprouter")
π Tip: Use descriptive names. They appear in logs and debugging output.
2. HTTP Methods
Lokstra supports all standard HTTP methods:
r.GET("/users", getUsersHandler)
r.POST("/users", createUserHandler)
r.PUT("/users/{id}", updateUserHandler)
r.PATCH("/users/{id}", patchUserHandler)
r.DELETE("/users/{id}", deleteUserHandler)
Special method:
// ANY matches all HTTP methods
r.ANY("/webhook", webhookHandler)
3. Path Parameters
Extract dynamic values from URLs:
type UserRequest struct {
ID string `path:"id"` // Auto-extracted from path
}
r.GET("/users/{id}", func(req *UserRequest) (string, error) {
return "User ID: " + req.ID, nil
})
Test it:
curl http://localhost:3000/users/123
# β "User ID: 123"
4. Query Parameters
Extract from query string:
type SearchRequest struct {
Query string `query:"q"`
Page int `query:"page"`
}
r.GET("/search", func(req *SearchRequest) (string, error) {
return fmt.Sprintf("Searching for: %s (page %d)", req.Query, req.Page), nil
})
Test it:
curl "http://localhost:3000/search?q=lokstra&page=2"
# β "Searching for: lokstra (page 2)"
π¨ Handler Forms (The Essential 4)
Lokstra supports 29 handler forms, but youβll use these 4 most often:
Form 1: Simple Return Value
Use when: Simple data, no errors
r.GET("/ping", func() string {
return "pong"
})
r.GET("/status", func() map[string]string {
return map[string]string{"status": "ok"}
})
Form 2: Return with Error
Use when: Operations that can fail (most common!)
r.GET("/users", func() ([]User, error) {
users, err := db.GetUsers()
if err != nil {
return nil, err // Lokstra handles error response
}
return users, nil
})
Form 3: Request Binding with Error
Use when: Need request data (POST/PUT)
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 is auto-bound from JSON body
user, err := db.CreateUser(req.Name, req.Email)
return user, err
})
Form 4: Context + Request with Error
Use when: Need full control (headers, status codes, etc)
r.GET("/users/{id}", func(ctx *request.Context, req *UserRequest) (*User, error) {
// Access request context
authHeader := ctx.R.Header.Get("Authorization")
// req.ID auto-extracted from path
user, err := db.GetUser(req.ID)
return user, err
})
π Which form to use?
- 90% of the time: Form 2 (return with error)
- POST/PUT endpoints: Form 3 (request binding)
- Need headers/cookies: Form 4 (with context)
- Ultra-simple: Form 1 (no errors possible)
π Want all 29 forms? See Deep Dive: Handler Forms
ποΈ Route Groups
Organize routes with shared prefixes:
Method 1: Inline Groups
r := lokstra.NewRouter("api")
// API v1
r.Group("/v1", func(v1 Router) {
v1.GET("/users", getUsersV1)
v1.GET("/products", getProductsV1)
})
// API v2
r.Group("/v2", func(v2 Router) {
v2.GET("/users", getUsersV2)
v2.GET("/products", getProductsV2)
})
Result:
GET /v1/users
GET /v1/products
GET /v2/users
GET /v2/products
Method 2: Stored Groups
v1 := r.AddGroup("/v1")
v1.GET("/users", getUsersV1)
v1.GET("/products", getProductsV1)
v2 := r.AddGroup("/v2")
v2.GET("/users", getUsersV2)
v2.GET("/products", getProductsV2)
π Tip: Use inline for simple cases, stored for complex routing logic.
π‘οΈ Middleware Basics
Lokstra supports 2 ways to use middleware:
Method 1: Direct Middleware Function
r := lokstra.NewRouter("api")
// Use middleware functions directly
r.Use(logging.Middleware(), auth.Middleware())
r.GET("/users", getUsersHandler)
r.POST("/users", createUserHandler)
// Both routes get logging + auth
Method 2: By Name (Registry-Based)
// First, register middleware factories (usually in main.go or setup)
lokstra_registry.RegisterMiddlewareFactory("logger", loggerFactory)
lokstra_registry.RegisterMiddlewareFactory("auth", authFactory)
// Then register named instances with config
lokstra_registry.RegisterMiddlewareName("logger_std", "logger", loggerStdConfig)
lokstra_registry.RegisterMiddlewareName("auth_jwt", "auth", jwtConfig)
// Use by name in router
r.Use("logger_std", "auth_jwt")
r.GET("/users", getUsersHandler)
π When to use which?
- Method 1: Simple apps, few middleware, code-only setup
- Method 2: Config-driven apps, reusable middleware with different configs
Example - Multiple auth configurations:
// Register factory once
lokstra_registry.RegisterMiddlewareFactory("auth", authFactory)
// Create named instances with different configs
lokstra_registry.RegisterMiddlewareName("auth_basic", "auth", basicConfig)
lokstra_registry.RegisterMiddlewareName("auth_jwt", "auth", jwtConfig)
lokstra_registry.RegisterMiddlewareName("auth_oauth", "auth", oauthConfig)
// Use different auth per router
publicAPI := lokstra.NewRouter("public")
publicAPI.Use("auth_basic")
adminAPI := lokstra.NewRouter("admin")
adminAPI.Use("auth_jwt")
Global Middleware
r := lokstra.NewRouter("api")
// Applied to ALL routes
r.Use(loggingMiddleware, corsMiddleware)
// Or by name:
r.Use("logger_std", "cors_default")
r.GET("/users", getUsersHandler)
r.POST("/users", createUserHandler)
// Both routes get logging + CORS
Per-Route Middleware
r.GET("/public", publicHandler) // No auth
// Method 1: Direct function
r.GET("/private", privateHandler, authMiddleware)
// Method 2: By name
r.GET("/private", privateHandler, "auth_jwt")
Group Middleware
admin := r.AddGroup("/admin")
admin.Use(authMiddleware, adminMiddleware)
// Or by name:
admin.Use("auth_jwt", "admin_check")
admin.GET("/users", getAllUsers) // Requires auth + admin
admin.DELETE("/users", deleteUser) // Requires auth + admin
r.GET("/public", publicEndpoint) // No middleware
π More on middleware: See 03 - Middleware
π§ͺ Examples
All examples are runnable! Navigate to each folder and go run main.go
Total learning time: ~45 minutes
01 - Basic Routes β±οΈ 5 min
Learn: Router basics, GET/POST, auto JSON conversion
r.GET("/ping", func() string { return "pong" })
r.GET("/users", func() []User { return users })
r.POST("/users", func(req *CreateUserRequest) (*User, error) { ... })
Key Concepts: Router creation, HTTP methods, automatic JSON, request binding, validation
02 - Route Parameters β±οΈ 7 min
Learn: Path params, query params, type conversion
// Path parameter
r.GET("/users/{id}", func(req *GetUserRequest) (*User, error) {
// req.ID extracted from path
})
// Query parameters
r.GET("/products", func(req *SearchRequest) ([]Product, error) {
// req.Category, req.MinPrice from ?category=x&min_price=y
})
Key Concepts: path:"id", query:"name", default values, automatic type conversion
03 - Route Groups β±οΈ 7 min
Learn: API versioning, route organization, nested groups
// API v1
v1 := r.AddGroup("/v1")
v1.GET("/users", getUsersV1)
// API v2
v2 := r.AddGroup("/v2")
v2.GET("/users", getUsersV2) // Enhanced version
// Nested groups
admin := r.AddGroup("/admin")
adminUsers := admin.AddGroup("/users")
Key Concepts: Route groups, prefixes, API versioning, PrintRoutes() debugging
04 - Handler Forms β±οΈ 10 min
Learn: 5 essential handler patterns, when to use each
// Form 1: Simple return
func() string { return "pong" }
// Form 2: With error (most common!)
func() ([]User, error) { return users, nil }
// Form 3: Request binding
func(req *CreateUserRequest) (*User, error) { ... }
// Form 4: Full control with context
func(ctx *request.Context, req *GetUserRequest) (*User, error) { ... }
// Form 5: Custom response
func(ctx *request.Context) (*response.Response, error) { ... }
Key Concepts: Handler signatures, flexibility, context access, decision guide
05 - Response Patterns β±οΈ 15 min β
Learn: 3 response types, 2 response paths, when to use each
3 Response Types:
// 1. Manual (http.ResponseWriter)
func(ctx *request.Context) error {
ctx.W.Write([]byte(`{"message":"hello"}`))
}
// 2. Generic (response.Response) - JSON, HTML, text
func() (*response.Response, error) {
resp := response.NewResponse()
resp.Json(data) // or .Html(), .Text()
return resp, nil
}
// 3. Opinionated (response.ApiHelper) - Structured JSON
func() (*response.ApiHelper, error) {
api := response.NewApiHelper()
api.Ok(data) // Standard format
return api, nil
}
Key Concepts:
- Manual vs Generic vs Opinionated responses
- When to use each type
- ApiHelper standard JSON format (success, error, validation)
- PagingRequest for list endpoints
- Decision guide for REST APIs
π― Common Patterns
Pattern 1: RESTful Resources
r.GET("/users", listUsers) // List
r.POST("/users", createUser) // Create
r.GET("/users/{id}", getUser) // Read
r.PUT("/users/{id}", updateUser) // Update
r.DELETE("/users/{id}", deleteUser) // Delete
Pattern 2: Nested Resources
r.GET("/users/{userId}/posts", getUserPosts)
r.POST("/users/{userId}/posts", createUserPost)
r.GET("/users/{userId}/posts/{postId}", getUserPost)
Pattern 3: API Versioning
// Option 1: Path-based
r.Group("/v1", func(v1 Router) { ... })
r.Group("/v2", func(v2 Router) { ... })
// Option 2: Subdomain (via multiple routers)
apiV1 := lokstra.NewRouter("api-v1")
apiV2 := lokstra.NewRouter("api-v2")
π« Common Mistakes
β Donβt: Register after Build()
r := lokstra.NewRouter("api")
r.GET("/first", handler1)
app := lokstra.NewApp("demo", ":8080", r)
app.Start() // Router builds here
r.GET("/second", handler2) // β PANIC! Can't register after build
β Do: Register all routes before starting
r.GET("/first", handler1)
r.GET("/second", handler2)
app.Start() // Now it's safe
β Donβt: Ignore errors in handlers
r.GET("/users", func() []User {
users, _ := db.GetUsers() // β Ignoring error!
return users
})
β Do: Return errors
r.GET("/users", func() ([]User, error) {
return db.GetUsers() // β
Error handled by Lokstra
})
π Best Practices
1. Use Meaningful Names
// β
Good
r := lokstra.NewRouter("user-api")
r := lokstra.NewRouter("admin-api")
// π« Bad
r := lokstra.NewRouter("r1")
r := lokstra.NewRouter("temp")
2. Group Related Routes
// β
Good
users := r.AddGroup("/users")
users.GET("", listUsers)
users.POST("", createUser)
users.GET("/{id}", getUser)
// π« Bad - harder to see relationships
r.GET("/users", listUsers)
r.POST("/users", createUser)
r.GET("/users/{id}", getUser)
3. Choose Right Handler Form
// β
Good - simple case
r.GET("/ping", func() string { return "pong" })
// β
Good - can fail
r.GET("/users", func() ([]User, error) { return db.GetUsers() })
// π« Overkill - don't need context
r.GET("/ping", func(ctx *request.Context) (string, error) {
return "pong", nil
})
4. Use ApiHelper for REST APIs
// β
Good - Consistent JSON structure
r.GET("/users", func() (*response.ApiHelper, error) {
api := response.NewApiHelper()
users, err := db.GetUsers()
if err != nil {
api.InternalError("Database error")
return api, nil
}
api.Ok(users)
return api, nil
})
// π« Inconsistent - no standard format
r.GET("/users", func() (map[string]any, error) {
users, err := db.GetUsers()
return map[string]any{"data": users}, err
})
5. Use PagingRequest for Lists
// β
Good - Standard pagination
type ListUsersRequest struct {
request.PagingRequest // Embeds page, page_size, order_by, etc
Status string `query:"status"`
}
r.GET("/users", func(req *ListUsersRequest) (*response.ApiHelper, error) {
req.SetDefaults() // Apply default page, page_size
users, total := db.GetUsers(req.GetOffset(), req.GetLimit())
api := response.NewApiHelper()
api.OkList(users, &api_formatter.ListMeta{
Page: req.Page,
PageSize: req.PageSize,
TotalRows: total,
})
return api, nil
})
// π« Bad - Reinventing pagination
type CustomPaging struct {
P int `query:"p"`
Sz int `query:"sz"`
}
π Whatβs Next?
You now understand:
- β Creating routers and registering routes
- β 5 essential handler forms (simple, error, binding, context, response)
- β Path and query parameters with automatic binding
- β Route groups for organization
- β 3 response patterns (Manual, Generic, Opinionated)
- β PagingRequest for list endpoints
- β Basic middleware usage
Next Steps:
Continue Learning (Recommended order):
- π 02 - Service - Service patterns and dependency injection β CRITICAL
- π 03 - Middleware - Deep dive into middleware patterns
- π 04 - Configuration - Config-driven development
- π 05 - App and Server - Application lifecycle
- π 06 - Complete API - Build a real TODO API
Deep Dive Topics:
- All 29 Handler Forms (coming soon)
- Router Lifecycle (coming soon)
- Advanced Routing (coming soon)
π Quick Reference
Common Methods
// Create
r := lokstra.NewRouter("name")
// HTTP Methods
r.GET(path, handler, middleware...)
r.POST(path, handler, middleware...)
r.PUT(path, handler, middleware...)
r.PATCH(path, handler, middleware...)
r.DELETE(path, handler, middleware...)
r.ANY(path, handler, middleware...)
// Groups
r.Group(prefix, func(Router))
g := r.AddGroup(prefix)
// Middleware
r.Use(middleware...)
// Debugging
r.PrintRoutes() // Print all registered routes
Continue learning β 02 - Service