| name | advanced-lokstra-middleware |
| description | Create custom middleware for request/response filtering, logging, authentication, and authorization. Use after framework setup to add cross-cutting concerns. |
| phase | advanced |
| order | 2 |
| license | MIT |
| compatibility | {"lokstra_version":">=0.1.0","go_version":">=1.21"} |
Advanced: Custom Middleware
When to Use
Use this skill when:
- Adding authentication/authorization checks
- Implementing request logging and metrics
- Creating rate limiting or throttling
- Adding request/response modification
- Handling cross-cutting concerns globally
Prerequisites:
- ✅ Framework initialized (see: implementation-lokstra-init-framework)
- ✅ Basic endpoints implemented
- ✅ Understand request flow
Core Concepts
Middleware Type Definition
type HandlerFunc func(c *Context) error
Key Context Methods
| Method | Description |
|---|
c.Next() | Calls the next middleware/handler in chain |
c.R | Access to *http.Request |
c.W | Access to response writer wrapper |
c.Api | API response helpers (Ok, Error, Unauthorized, etc.) |
c.Set(key, value) | Store value in context |
c.Get(key) | Retrieve value from context |
c.StatusCode() | Get response status code |
Middleware Architecture
Request Flow
Request
↓
Middleware 1 (pre-processing)
↓
Middleware 2 (pre-processing)
↓
Middleware 3 (pre-processing)
↓
Handler
↓
Middleware 3 (post-processing)
↓
Middleware 2 (post-processing)
↓
Middleware 1 (post-processing)
↓
Response
Middleware Execution Order (Recommended)
middlewares:
- type: recovery
- type: request_logger
- type: slow_request_logger
- type: cors
- type: body_limit
- type: gzip_compression
Why this order?
- Recovery first - Catches panics from all other middleware
- Logging early - Records all requests, even failed ones
- CORS early - Handles preflight before authentication
- Body limit before parsing - Prevents memory exhaustion
- Compression last - Compresses final response
Built-in Middleware
Lokstra provides 6 built-in middleware packages:
| Middleware | Package | Config Type |
|---|
| Recovery | middleware/recovery | recovery |
| Request Logger | middleware/request_logger | request_logger |
| Slow Request Logger | middleware/slow_request_logger | slow_request_logger |
| CORS | middleware/cors | cors |
| Body Limit | middleware/body_limit | body_limit |
| Gzip Compression | middleware/gzipcompression | gzip_compression |
Register Built-in Middleware
import (
"github.com/primadi/lokstra/middleware/recovery"
"github.com/primadi/lokstra/middleware/request_logger"
"github.com/primadi/lokstra/middleware/cors"
"github.com/primadi/lokstra/middleware/body_limit"
"github.com/primadi/lokstra/middleware/gzipcompression"
)
func registerMiddleware() {
recovery.Register()
request_logger.Register()
cors.Register()
body_limit.Register()
gzipcompression.Register()
}
Creating Custom Middleware
Standard Pattern (3 Components)
Every Lokstra middleware should have these 3 components:
- Config struct - Configuration options
- Middleware function - Returns
request.HandlerFunc
- MiddlewareFactory function - For YAML configuration support
- Register function - Registers with lokstra_registry
Template: Custom Middleware
File: middleware/your_middleware/your_middleware.go
package your_middleware
import (
"github.com/primadi/lokstra/common/utils"
"github.com/primadi/lokstra/core/request"
"github.com/primadi/lokstra/lokstra_registry"
)
const YOUR_MIDDLEWARE_TYPE = "your_middleware"
const PARAMS_OPTION1 = "option1"
const PARAMS_OPTION2 = "option2"
type Config struct {
Option1 string
Option2 int
}
func DefaultConfig() *Config {
return &Config{
Option1: "default_value",
Option2: 100,
}
}
func Middleware(cfg *Config) request.HandlerFunc {
defConfig := DefaultConfig()
if cfg == nil {
cfg = defConfig
}
if cfg.Option1 == "" {
cfg.Option1 = defConfig.Option1
}
if cfg.Option2 <= 0 {
cfg.Option2 = defConfig.Option2
}
return request.HandlerFunc(func(c *request.Context) error {
err := c.Next()
return err
})
}
func MiddlewareFactory(params map[string]any) request.HandlerFunc {
defConfig := DefaultConfig()
if params == nil {
return Middleware(defConfig)
}
cfg := &Config{
Option1: utils.GetValueFromMap(params, PARAMS_OPTION1, defConfig.Option1),
Option2: utils.GetValueFromMap(params, PARAMS_OPTION2, defConfig.Option2),
}
return Middleware(cfg)
}
func Register() {
lokstra_registry.RegisterMiddlewareFactory(YOUR_MIDDLEWARE_TYPE, MiddlewareFactory,
lokstra_registry.AllowOverride(true))
}
Example: Authentication Middleware
File: middleware/auth/auth.go
package auth
import (
"strings"
"github.com/primadi/lokstra/common/logger"
"github.com/primadi/lokstra/common/utils"
"github.com/primadi/lokstra/core/request"
"github.com/primadi/lokstra/lokstra_registry"
)
const AUTH_TYPE = "auth"
const PARAMS_TOKEN_PREFIX = "token_prefix"
const PARAMS_HEADER_NAME = "header_name"
type Config struct {
TokenPrefix string
HeaderName string
ValidateFunc func(token string) (map[string]any, error)
}
func DefaultConfig() *Config {
return &Config{
TokenPrefix: "Bearer",
HeaderName: "Authorization",
ValidateFunc: nil,
}
}
func Middleware(cfg *Config) request.HandlerFunc {
defConfig := DefaultConfig()
if cfg == nil {
cfg = defConfig
}
if cfg.TokenPrefix == "" {
cfg.TokenPrefix = defConfig.TokenPrefix
}
if cfg.HeaderName == "" {
cfg.HeaderName = defConfig.HeaderName
}
return request.HandlerFunc(func(c *request.Context) error {
authHeader := c.R.Header.Get(cfg.HeaderName)
if authHeader == "" {
logger.LogInfo("🔒 [auth] Missing %s header", cfg.HeaderName)
return c.Api.Unauthorized("Missing authorization header")
}
prefix := cfg.TokenPrefix + " "
if len(authHeader) < len(prefix) || !strings.HasPrefix(authHeader, prefix) {
logger.LogInfo("🔒 [auth] Invalid authorization format")
return c.Api.Unauthorized("Invalid authorization format")
}
token := authHeader[len(prefix):]
if token == "" {
return c.Api.Unauthorized("Empty token")
}
if cfg.ValidateFunc != nil {
claims, err := cfg.ValidateFunc(token)
if err != nil {
logger.LogInfo("🔒 [auth] Invalid token: %v", err)
return c.Api.Unauthorized("Invalid token")
}
for key, value := range claims {
c.Set(key, value)
}
}
c.Set("authenticated", true)
logger.LogInfo("✅ [auth] Authenticated")
return c.Next()
})
}
func MiddlewareFactory(params map[string]any) request.HandlerFunc {
defConfig := DefaultConfig()
if params == nil {
return Middleware(defConfig)
}
cfg := &Config{
TokenPrefix: utils.GetValueFromMap(params, PARAMS_TOKEN_PREFIX, defConfig.TokenPrefix),
HeaderName: utils.GetValueFromMap(params, PARAMS_HEADER_NAME, defConfig.HeaderName),
ValidateFunc: nil,
}
return Middleware(cfg)
}
func Register() {
lokstra_registry.RegisterMiddlewareFactory(AUTH_TYPE, MiddlewareFactory,
lokstra_registry.AllowOverride(true))
}
Example: Rate Limiting Middleware
File: middleware/ratelimit/ratelimit.go
package ratelimit
import (
"sync"
"time"
"github.com/primadi/lokstra/common/utils"
"github.com/primadi/lokstra/core/request"
"github.com/primadi/lokstra/lokstra_registry"
)
const RATELIMIT_TYPE = "ratelimit"
const PARAMS_REQUESTS_PER_SECOND = "requests_per_second"
const PARAMS_BURST = "burst"
type Config struct {
RequestsPerSecond int
Burst int
}
func DefaultConfig() *Config {
return &Config{
RequestsPerSecond: 100,
Burst: 10,
}
}
type clientLimiter struct {
tokens int
lastReset time.Time
}
type RateLimiter struct {
config *Config
mu sync.Mutex
clients map[string]*clientLimiter
}
func Middleware(cfg *Config) request.HandlerFunc {
defConfig := DefaultConfig()
if cfg == nil {
cfg = defConfig
}
if cfg.RequestsPerSecond <= 0 {
cfg.RequestsPerSecond = defConfig.RequestsPerSecond
}
if cfg.Burst <= 0 {
cfg.Burst = defConfig.Burst
}
limiter := &RateLimiter{
config: cfg,
clients: make(map[string]*clientLimiter),
}
return request.HandlerFunc(func(c *request.Context) error {
clientIP := c.R.RemoteAddr
limiter.mu.Lock()
client, exists := limiter.clients[clientIP]
if !exists {
client = &clientLimiter{
tokens: cfg.RequestsPerSecond + cfg.Burst,
lastReset: time.Now(),
}
limiter.clients[clientIP] = client
}
now := time.Now()
if now.Sub(client.lastReset) >= time.Second {
client.tokens = cfg.RequestsPerSecond + cfg.Burst
client.lastReset = now
}
if client.tokens <= 0 {
limiter.mu.Unlock()
return c.Api.Error(429, "RATE_LIMITED", "Too many requests")
}
client.tokens--
limiter.mu.Unlock()
return c.Next()
})
}
func MiddlewareFactory(params map[string]any) request.HandlerFunc {
defConfig := DefaultConfig()
if params == nil {
return Middleware(defConfig)
}
cfg := &Config{
RequestsPerSecond: utils.GetValueFromMap(params, PARAMS_REQUESTS_PER_SECOND, defConfig.RequestsPerSecond),
Burst: utils.GetValueFromMap(params, PARAMS_BURST, defConfig.Burst),
}
return Middleware(cfg)
}
func Register() {
lokstra_registry.RegisterMiddlewareFactory(RATELIMIT_TYPE, MiddlewareFactory,
lokstra_registry.AllowOverride(true))
}
Example: Request ID Middleware
File: middleware/request_id/request_id.go
package request_id
import (
"github.com/google/uuid"
"github.com/primadi/lokstra/common/utils"
"github.com/primadi/lokstra/core/request"
"github.com/primadi/lokstra/lokstra_registry"
)
const REQUEST_ID_TYPE = "request_id"
const PARAMS_HEADER_NAME = "header_name"
const PARAMS_CONTEXT_KEY = "context_key"
type Config struct {
HeaderName string
ContextKey string
}
func DefaultConfig() *Config {
return &Config{
HeaderName: "X-Request-ID",
ContextKey: "request_id",
}
}
func Middleware(cfg *Config) request.HandlerFunc {
defConfig := DefaultConfig()
if cfg == nil {
cfg = defConfig
}
if cfg.HeaderName == "" {
cfg.HeaderName = defConfig.HeaderName
}
if cfg.ContextKey == "" {
cfg.ContextKey = defConfig.ContextKey
}
return request.HandlerFunc(func(c *request.Context) error {
requestID := c.R.Header.Get(cfg.HeaderName)
if requestID == "" {
requestID = uuid.New().String()
}
c.Set(cfg.ContextKey, requestID)
c.W.Header().Set(cfg.HeaderName, requestID)
return c.Next()
})
}
func MiddlewareFactory(params map[string]any) request.HandlerFunc {
defConfig := DefaultConfig()
if params == nil {
return Middleware(defConfig)
}
cfg := &Config{
HeaderName: utils.GetValueFromMap(params, PARAMS_HEADER_NAME, defConfig.HeaderName),
ContextKey: utils.GetValueFromMap(params, PARAMS_CONTEXT_KEY, defConfig.ContextKey),
}
return Middleware(cfg)
}
func Register() {
lokstra_registry.RegisterMiddlewareFactory(REQUEST_ID_TYPE, MiddlewareFactory,
lokstra_registry.AllowOverride(true))
}
Registering Custom Middleware
Option 1: Using Register() Function
package main
import (
"github.com/primadi/lokstra"
"github.com/primadi/lokstra/lokstra_init"
"github.com/primadi/lokstra/middleware/recovery"
"github.com/primadi/lokstra/middleware/cors"
"github.com/primadi/lokstra/middleware/request_logger"
"myapp/middleware/auth"
"myapp/middleware/ratelimit"
_ "myapp/modules/user/application"
)
func main() {
lokstra.Bootstrap()
recovery.Register()
request_logger.Register()
cors.Register()
auth.Register()
ratelimit.Register()
lokstra_init.BootstrapAndRun()
}
Option 2: Inline Registration with Factory
package main
import (
"github.com/primadi/lokstra/common/logger"
"github.com/primadi/lokstra/core/request"
"github.com/primadi/lokstra/lokstra_registry"
)
func registerMiddleware() {
lokstra_registry.RegisterMiddlewareFactory("request-logger", func(config map[string]any) request.HandlerFunc {
return func(ctx *request.Context) error {
logger.LogInfo("→ %s %s", ctx.R.Method, ctx.R.URL.Path)
err := ctx.Next()
logger.LogInfo("← %s %s (status: %d)", ctx.R.Method, ctx.R.URL.Path, ctx.StatusCode())
return err
}
})
lokstra_registry.RegisterMiddlewareFactory("simple-auth", func(config map[string]any) request.HandlerFunc {
tokenPrefix := "Bearer"
if prefix, ok := config["token_prefix"].(string); ok {
tokenPrefix = prefix
}
return func(ctx *request.Context) error {
authHeader := ctx.R.Header.Get("Authorization")
if authHeader == "" {
return ctx.Api.Unauthorized("Missing authorization header")
}
expectedPrefix := tokenPrefix + " "
if len(authHeader) < len(expectedPrefix) {
return ctx.Api.Unauthorized("Invalid authorization format")
}
token := authHeader[len(expectedPrefix):]
ctx.Set("token", token)
ctx.Set("authenticated", true)
return ctx.Next()
}
})
}
YAML Configuration
Global Middleware
File: configs/middlewares.yaml
middlewares:
- type: recovery
params:
enable_stack_trace: false
enable_logging: true
- type: request_logger
params:
enable_colors: true
skip_paths: ["/health", "/metrics"]
- type: slow_request_logger
params:
threshold: 500
enable_colors: true
- type: cors
params:
allow_origins: ["*"]
- type: body_limit
params:
max_size: 10485760
skip_on_path: ["/upload/**"]
- type: gzip_compression
params:
min_size: 1024
compression_level: -1
- type: auth
params:
token_prefix: Bearer
header_name: Authorization
- type: ratelimit
params:
requests_per_second: 100
burst: 10
Per-Route Middleware
Using @Route Decorators
type UserHandler struct {
}
func (h *UserHandler) GetProfile(ctx *request.Context) error {
userID := ctx.Get("user_id")
}
func (h *UserHandler) Delete(ctx *request.Context, id string) error {
}
func (h *UserHandler) Login(req *LoginRequest) (*TokenResponse, error) {
}
Best Practices
1. Use Standard Pattern
✅ Always include:
- Config struct with documented fields
- DefaultConfig() function
- Middleware(cfg *Config) function
- MiddlewareFactory(params map[string]any) function
- Register() function
❌ Don't:
- Use struct methods for middleware
- Use router.HandlerFunc (use request.HandlerFunc)
- Forget to call c.Next() for passthrough
2. Middleware Order Matters
middlewares:
- type: recovery
- type: request_logger
- type: cors
- type: body_limit
- type: auth
- type: gzip_compression
3. Fail Fast
func Middleware(cfg *Config) request.HandlerFunc {
return request.HandlerFunc(func(c *request.Context) error {
if !isValid(c) {
return c.Api.Unauthorized("Access denied")
}
return c.Next()
})
}
func Middleware(cfg *Config) request.HandlerFunc {
return request.HandlerFunc(func(c *request.Context) error {
if !isValid(c) {
c.Api.Unauthorized("Access denied")
}
return c.Next()
})
}
4. Use Context for Request Data
func (c *request.Context) error {
c.Set("user_id", "123")
c.Set("role", "admin")
return c.Next()
}
userID := ctx.Get("user_id").(string)
var currentUserID string
5. Thread-Safe Shared State
type RateLimiter struct {
mu sync.Mutex
clients map[string]*clientLimiter
}
func (rl *RateLimiter) check(clientIP string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
}
var clients = make(map[string]int)
Testing Middleware
Unit Test Pattern
package auth_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/primadi/lokstra/core/request"
"github.com/stretchr/testify/assert"
"myapp/middleware/auth"
)
func TestAuthMiddleware_MissingHeader(t *testing.T) {
mw := auth.Middleware(&auth.Config{
TokenPrefix: "Bearer",
HeaderName: "Authorization",
})
req := httptest.NewRequest("GET", "/api/users", nil)
rec := httptest.NewRecorder()
handlers := []request.HandlerFunc{mw}
ctx := request.NewContext(rec, req, handlers)
err := mw(ctx)
assert.Error(t, err)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
}
func TestAuthMiddleware_ValidToken(t *testing.T) {
nextCalled := false
nextHandler := request.HandlerFunc(func(c *request.Context) error {
nextCalled = true
return nil
})
mw := auth.Middleware(&auth.Config{
TokenPrefix: "Bearer",
HeaderName: "Authorization",
})
req := httptest.NewRequest("GET", "/api/users", nil)
req.Header.Set("Authorization", "Bearer valid-token-123")
rec := httptest.NewRecorder()
handlers := []request.HandlerFunc{mw, nextHandler}
ctx := request.NewContext(rec, req, handlers)
err := mw(ctx)
assert.NoError(t, err)
assert.True(t, nextCalled)
assert.Equal(t, true, ctx.Get("authenticated"))
}
Integration Test with Router
func TestMiddlewareIntegration(t *testing.T) {
router := lokstra.NewRouter()
router.Use(
recovery.Middleware(&recovery.Config{}),
auth.Middleware(&auth.Config{}),
)
router.GET("/protected", func(c *request.Context) error {
return c.Api.Ok(map[string]any{
"user_id": c.Get("user_id"),
})
})
req1 := httptest.NewRequest("GET", "/protected", nil)
rec1 := httptest.NewRecorder()
router.ServeHTTP(rec1, req1)
assert.Equal(t, 401, rec1.Code)
req2 := httptest.NewRequest("GET", "/protected", nil)
req2.Header.Set("Authorization", "Bearer test-token")
rec2 := httptest.NewRecorder()
router.ServeHTTP(rec2, req2)
assert.Equal(t, 200, rec2.Code)
}
Common Patterns
Conditional Middleware
func Middleware(cfg *Config) request.HandlerFunc {
return request.HandlerFunc(func(c *request.Context) error {
if shouldSkip(c.R.URL.Path, cfg.SkipPaths) {
return c.Next()
}
return c.Next()
})
}
func shouldSkip(path string, skipPaths []string) bool {
for _, skip := range skipPaths {
if path == skip || strings.HasPrefix(path, skip) {
return true
}
}
return false
}
Timing Middleware
func Middleware(cfg *Config) request.HandlerFunc {
return request.HandlerFunc(func(c *request.Context) error {
start := time.Now()
err := c.Next()
duration := time.Since(start)
logger.LogInfo("[%s] %s - %v", c.R.Method, c.R.URL.Path, duration)
return err
})
}
Response Modification
func Middleware(cfg *Config) request.HandlerFunc {
return request.HandlerFunc(func(c *request.Context) error {
c.W.Header().Set("X-Custom-Header", "value")
err := c.Next()
c.W.Header().Set("X-Response-Time", time.Since(start).String())
return err
})
}
Checklist: Creating Middleware
Related Skills