Example 08: Middleware
β±οΈ 30 minutes β’ π― Intermediate
Master middleware patterns in Lokstra
Learn how to use middleware for cross-cutting concerns like authentication, logging, recovery, and rate limiting.
π What Youβll Learn
- β Built-in middlewares: CORS, Recovery, Request Logger
- β Custom middlewares: Auth, Rate limiting, Custom logging
- β Global middlewares: Apply to all routes
- β Route-specific middlewares: Apply to specific endpoints
- β Middleware chaining: Multiple middlewares in sequence
- β Middleware factory pattern: Create configurable middlewares
- β Context access: Share data between middlewares and handlers
- β Config-based middleware: Define middleware in YAML config
- β Lazy middleware resolution: Use middleware by name before config loads
ποΈ Architecture
Request Flow:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client Request β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Global Middlewares (Applied to ALL routes) β
β 1. Recovery β Catch panics β
β 2. CORS β Cross-origin handling β
β 3. Logger β Request/response logging β
β 4. Custom Logger β Additional logging β
β 5. Rate Limiter β Prevent abuse β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Route-Specific Middlewares β
β - Auth β Check API key β
β - Admin Check β Verify admin role β
β - Custom β Any route-specific logic β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Handler β Business Logic β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Response β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Running the Example
cd docs/00-introduction/examples/08-middleware
go run main.go
The server starts on http://localhost:3000
π Testing
Use the provided test.http file with VS Code REST Client extension, or use curl:
Public Endpoints (No Auth)
# Basic endpoint
curl http://localhost:3000/
# Public endpoint
curl http://localhost:3000/public
# Health check
curl http://localhost:3000/health
Protected Endpoints (Requires Auth)
# Without API key β 401 Unauthorized
curl http://localhost:3000/protected
# With wrong key β 403 Forbidden
curl -H "X-API-Key: wrong-key" http://localhost:3000/protected
# With valid key β 200 OK
curl -H "X-API-Key: secret-key-123" http://localhost:3000/protected
# Profile endpoint
curl -H "X-API-Key: secret-key-123" http://localhost:3000/api/profile
Admin Endpoints (Requires Admin Key)
# With regular key β 403 Forbidden
curl -H "X-API-Key: secret-key-123" http://localhost:3000/api/admin/dashboard
# With admin key β 200 OK
curl -H "X-API-Key: admin-key-456" http://localhost:3000/api/admin/dashboard
Test Middlewares
# Test panic recovery
curl http://localhost:3000/panic
# Test slow request logging
curl http://localhost:3000/slow
# Test middleware chain
curl http://localhost:3000/chain
π Key Concepts
1. Config-Based Middleware (Recommended)
Define middlewares in config.yaml for easy configuration management:
middleware-definitions:
recovery-prod:
type: recovery
config:
log-stack-trace: true
include-stack-in-response: false
cors-api:
type: cors
config:
allowed-origins: ["*"]
allowed-methods: ["GET", "POST", "PUT", "DELETE"]
Then use them by name in code:
// IMPORTANT: Middleware resolution is LAZY!
// You can Use() middleware names BEFORE loading config
// Step 1: Create router and register routes
r := lokstra.NewRouter("api")
r.Use("recovery-prod") // β OK! String stored, not resolved yet
r.Use("cors-api") // β OK! Lazy resolution
// Step 2: Load config (can be done AFTER router setup!)
lokstra_registry.LoadAndBuild([]string{"config.yaml"})
// Step 3: Build router - all middleware names resolved here
r.Build() // or app.Run() which calls Build()
// Or route-specific
r.GET("/api/users", handler, "jwt-auth", "cors-api")
Benefits:
- β Change configuration without rebuilding
- β Cleaner code (no CreateMiddleware calls)
- β Centralized middleware management
- β Easy to swap configs per environment (dev/prod)
- β Lazy resolution - define routes before loading config!
2. Built-in Middlewares
Lokstra provides ready-to-use middlewares:
// Register factories
cors.Register()
recovery.Register()
request_logger.Register()
// Programmatic registration (alternative to config)
lokstra_registry.RegisterMiddlewareName("cors-all", cors.CORS_TYPE, map[string]any{
"allow_origins": []string{"*"},
})
// Use them
r.Use("cors-all") // String name (if registered or in config)
// OR
r.Use(lokstra_registry.CreateMiddleware("cors-all")) // Old way
Available Built-in Middlewares:
cors- CORS handlingrecovery- Panic recoveryrequest_logger- HTTP request/response loggingslow_request_logger- Slow request detectiongzipcompression- Response compressionbody_limit- Request body size limitsjwtauth- JWT authenticationaccesscontrol- Role-based access control
3. Custom Middleware
Simple middleware signature: func(*request.Context) error
func CustomAuthMiddleware(ctx *request.Context) error {
apiKey := ctx.R.Header.Get("X-API-Key")
if apiKey == "" {
return ctx.Api.Unauthorized("Missing API key")
}
// Store data for later use
ctx.Set("api_key", apiKey)
return nil // Continue to next middleware/handler
}
4. Middleware Factory Pattern
Create configurable middleware:
func RateLimitMiddleware(maxRequests int, window time.Duration) request.HandlerFunc {
requests := make(map[string][]time.Time)
return func(ctx *request.Context) error {
ip := ctx.R.RemoteAddr
// ... rate limit logic
return nil
}
}
// Use it
r.Use(RateLimitMiddleware(10, time.Minute)) // 10 requests per minute
5. Global vs Route-Specific
// Global - Applied to ALL routes
r.Use(RecoveryMiddleware)
r.Use(CORSMiddleware)
// Route-specific - Only for this endpoint
r.GET("/protected", ProtectedHandler, AuthMiddleware)
// Multiple middlewares for one route (executed in order)
r.GET("/admin", AdminHandler, AuthMiddleware, AdminCheckMiddleware)
5. Middleware Chaining
Middlewares execute in order:
r.GET("/endpoint",
Handler,
Middleware1, // Runs first
Middleware2, // Runs second
Middleware3, // Runs third
)
Each middleware must call ctx.Next() or return to continue the chain:
func LoggingMiddleware(ctx *request.Context) error {
start := time.Now()
// Before handler
log.Println("Before:", ctx.R.URL.Path)
// Execute next middleware/handler
err := ctx.Next()
// After handler
duration := time.Since(start)
log.Println("After:", duration)
return err
}
6. Context Sharing
Share data between middlewares and handlers:
// In middleware
func AuthMiddleware(ctx *request.Context) error {
user := authenticate(ctx)
ctx.Set("user", user) // Store in context
return nil
}
// In handler
func ProfileHandler(ctx *request.Context) map[string]any {
user := ctx.Get("user") // Retrieve from context
return map[string]any{
"user": user,
}
}
π― Common Middleware Patterns
Authentication
func AuthMiddleware(ctx *request.Context) error {
token := ctx.R.Header.Get("Authorization")
if token == "" {
return ctx.Api.Unauthorized("Missing token")
}
user, err := validateToken(token)
if err != nil {
return ctx.Api.Forbidden("Invalid token")
}
ctx.Set("user", user)
return nil
}
Authorization
func AdminOnlyMiddleware(ctx *request.Context) error {
user := ctx.Get("user").(*User)
if !user.IsAdmin {
return ctx.Api.Forbidden("Admin access required")
}
return nil
}
Rate Limiting
func RateLimitMiddleware(max int, window time.Duration) request.HandlerFunc {
limiter := rate.NewLimiter(rate.Every(window/time.Duration(max)), max)
return func(ctx *request.Context) error {
if !limiter.Allow() {
return ctx.Api.Error(429, "RATE_LIMIT", "Too many requests")
}
return nil
}
}
Logging
func LoggingMiddleware(ctx *request.Context) error {
start := time.Now()
err := ctx.Next()
log.Printf("%s %s - %d (%v)",
ctx.R.Method,
ctx.R.URL.Path,
ctx.W.StatusCode(),
time.Since(start))
return err
}
Error Recovery
func RecoveryMiddleware(ctx *request.Context) error {
defer func() {
if r := recover(); r != nil {
log.Printf("PANIC: %v\n%s", r, debug.Stack())
ctx.Api.Error(500, "INTERNAL_ERROR", "Internal server error")
}
}()
return ctx.Next()
}
π Execution Order
Request β Global MW 1 β Global MW 2 β Route MW 1 β Route MW 2 β Handler
β β β β β
ctx.Next() ctx.Next() ctx.Next() ctx.Next() return
β β β β β
Response β Global MW 1 β Global MW 2 β Route MW 1 β Route MW 2 β Handler
Each middleware can:
- Execute code before the handler (
ctx.Next()) - Execute code after the handler (after
ctx.Next()returns) - Short-circuit the chain by returning early
- Modify the request or response
- Store data in context
π‘ Best Practices
- Order Matters: Recovery should be first, logging second
- Global vs Specific: Use global for cross-cutting concerns, route-specific for targeted logic
- Fail Fast: Auth/validation middlewares should fail early
- Share Data: Use
ctx.Set()/ctx.Get()to share between middlewares - Error Handling: Return errors properly, donβt panic
- Performance: Keep middlewares lightweight, avoid heavy operations
- Reusability: Create factory functions for configurable middlewares
π Comparison: Global vs Route-Specific
Global Middleware
// Applied to ALL routes
r.Use(RecoveryMiddleware)
r.Use(LoggingMiddleware)
r.GET("/public", PublicHandler) // Has Recovery + Logging
r.GET("/private", PrivateHandler) // Has Recovery + Logging
Use for:
- Recovery from panics
- CORS headers
- Request logging
- Rate limiting (global)
- Response compression
Route-Specific Middleware
// Applied only to specific routes
r.GET("/public", PublicHandler) // No auth
r.GET("/private", PrivateHandler, AuthMiddleware) // Has auth
r.GET("/admin", AdminHandler, AuthMiddleware, AdminMW) // Has auth + admin
Use for:
- Authentication
- Authorization
- Input validation
- Request-specific logging
- Feature flags
π Learning Path
- Start Simple: Global recovery and logging
- Add Auth: Implement authentication middleware
- Authorization: Add role-based access control
- Rate Limiting: Prevent abuse
- Custom Logic: Build your own middlewares
π Next Steps
- Example 06: Auto-Router (coming soon) - Combine middleware with auto-generated routes
- Built-in Middlewares Documentation - Deep dive into all built-in middlewares
- Custom Middleware Guide - Advanced patterns
π Additional Documentation
- NAMING-CONVENTIONS - Middleware naming best practices
- RECOVERY-ANALYSIS - Why recovery middleware is critical
- RATE-LIMIT-PERFORMANCE - Performance analysis
β οΈ Important Notes
Recovery Middleware is CRITICAL
Neither Goβs ServeMux nor Chi router auto-recover from panics!
Without recovery middleware, a single panic will crash your entire server. All users will be disconnected and the server must be manually restarted.
β Always use recovery middleware in production:
// Development
r.Use("recovery-dev") // Shows stack traces
// Production
r.Use("recovery-prod") // Hides stack traces from clients
See RECOVERY-ANALYSIS for detailed explanation.
Naming Conventions
For consistency across the framework:
- Factory types: Use
snake_case(e.g.,request_logger,cors) - Instance names: Use
kebab-case(e.g.,request-logger-verbose,cors-api)
See NAMING-CONVENTIONS for complete guide.
Performance Notes
Endpoint Response Times
- Fast endpoints (
/,/public,/health): ~10-20ms- Middleware overhead: < 2ms
- Handler: < 1ms
- Network latency: ~10ms
- Slow endpoint (
/slow): ~2000ms- Intentionally slow (simulates long-running operation)
- Uses
time.Sleep(2 * time.Second)for testing
Middleware Overhead
Each middleware adds minimal overhead:
- RateLimitMiddleware: ~0.1-0.2ms
- LoggingMiddleware: ~0.05ms
- CORS: ~0.01ms
- Recovery: ~0.01ms
Total middleware overhead: < 2ms
Rate limiting does NOT slow down successful requests - itβs just an in-memory map check.
See RATE-LIMIT-PERFORMANCE for benchmark details.
ctx.Next() is MANDATORY
CRITICAL: In Lokstra, middleware MUST call ctx.Next() to continue the chain:
func MyMiddleware(ctx *request.Context) error {
// Pre-processing
log.Println("Before handler")
// β
MUST call ctx.Next() to continue
err := ctx.Next()
// Post-processing
log.Println("After handler")
return err
}
Without ctx.Next(), the chain stops and handlers never execute!
func BrokenMiddleware(ctx *request.Context) error {
log.Println("Processing...")
return nil // β Chain stops here! Handler never runs!
}
Key Takeaway: Middlewares are the backbone of cross-cutting concerns in Lokstra. Master them for clean, maintainable APIs! π―