Documentation Index
Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The feature middleware provides feature flag support for controlled rollouts, A/B testing, and gradual feature deployment. It supports static flags, in-memory providers, and custom providers.
Installation
import "github.com/go-mizu/mizu/middlewares/feature"
Quick Start
app := mizu.New()
// Static feature flags
app.Use(feature.New(feature.Flags{
"dark_mode": &feature.Flag{Name: "dark_mode", Enabled: true},
"new_checkout": &feature.Flag{Name: "new_checkout", Enabled: false},
}))
app.Get("/", func(c *mizu.Ctx) error {
if feature.IsEnabled(c, "dark_mode") {
return c.JSON(200, darkModeResponse)
}
return c.JSON(200, normalResponse)
})
Configuration
| Option | Type | Default | Description |
|---|
Provider | Provider | Static | Flag provider |
Flags | Flags | - | Static flags map |
Flag Structure
type Flag struct {
Name string
Enabled bool
Description string
Metadata map[string]any
}
Examples
Static Flags
app.Use(feature.New(feature.Flags{
"feature_a": &feature.Flag{
Name: "feature_a",
Enabled: true,
Description: "New dashboard design",
},
"feature_b": &feature.Flag{
Name: "feature_b",
Enabled: false,
},
}))
Check Flag in Handler
app.Get("/dashboard", func(c *mizu.Ctx) error {
if feature.IsEnabled(c, "new_dashboard") {
return renderNewDashboard(c)
}
return renderOldDashboard(c)
})
Get Flag Details
app.Get("/features", func(c *mizu.Ctx) error {
flag := feature.Get(c, "beta_feature")
if flag != nil {
return c.JSON(200, map[string]any{
"name": flag.Name,
"enabled": flag.Enabled,
"description": flag.Description,
})
}
return c.JSON(404, "Feature not found")
})
Memory Provider (Dynamic)
// Create a mutable provider
provider := feature.NewMemoryProvider()
app.Use(feature.WithProvider(provider))
// Enable/disable flags at runtime
app.Post("/admin/features/:name/enable", func(c *mizu.Ctx) error {
provider.Enable(c.Param("name"))
return c.Text(200, "Enabled")
})
app.Post("/admin/features/:name/disable", func(c *mizu.Ctx) error {
provider.Disable(c.Param("name"))
return c.Text(200, "Disabled")
})
app.Post("/admin/features/:name/toggle", func(c *mizu.Ctx) error {
provider.Toggle(c.Param("name"))
return c.Text(200, "Toggled")
})
Require Flag Middleware
// Route requires feature to be enabled
app.Get("/beta",
feature.Require("beta_access", nil),
func(c *mizu.Ctx) error {
return c.Text(200, "Welcome to beta!")
},
)
// Custom handler when disabled
app.Get("/new-feature",
feature.Require("new_feature", func(c *mizu.Ctx) error {
return c.JSON(403, map[string]string{
"error": "Feature not available",
})
}),
newFeatureHandler,
)
Require All Flags
// Requires ALL flags to be enabled
app.Get("/admin/super",
feature.RequireAll([]string{"admin_access", "super_powers"}, nil),
superAdminHandler,
)
Require Any Flag
// Requires ANY flag to be enabled
app.Get("/premium",
feature.RequireAny([]string{"premium_user", "trial_active"}, nil),
premiumHandler,
)
Custom Provider
type DatabaseProvider struct {
db *sql.DB
}
func (p *DatabaseProvider) GetFlags(c *mizu.Ctx) (feature.Flags, error) {
rows, err := p.db.Query("SELECT name, enabled FROM feature_flags")
if err != nil {
return nil, err
}
defer rows.Close()
flags := make(feature.Flags)
for rows.Next() {
var name string
var enabled bool
rows.Scan(&name, &enabled)
flags[name] = &feature.Flag{Name: name, Enabled: enabled}
}
return flags, nil
}
app.Use(feature.WithProvider(&DatabaseProvider{db: db}))
User-Specific Flags
type UserProvider struct {
db *sql.DB
}
func (p *UserProvider) GetFlags(c *mizu.Ctx) (feature.Flags, error) {
userID := c.Get("user_id").(string)
// Get user-specific flags
flags := make(feature.Flags)
// Check if user is in beta program
var inBeta bool
p.db.QueryRow("SELECT beta FROM users WHERE id = ?", userID).Scan(&inBeta)
flags["beta_features"] = &feature.Flag{Name: "beta_features", Enabled: inBeta}
return flags, nil
}
Percentage Rollout
type RolloutProvider struct {
percentages map[string]int
}
func (p *RolloutProvider) GetFlags(c *mizu.Ctx) (feature.Flags, error) {
userID := c.Get("user_id").(string)
hash := hashUserID(userID)
flags := make(feature.Flags)
for name, pct := range p.percentages {
flags[name] = &feature.Flag{
Name: name,
Enabled: (hash % 100) < pct,
}
}
return flags, nil
}
List All Flags
app.Get("/features", func(c *mizu.Ctx) error {
flags := feature.GetFlags(c)
result := make([]map[string]any, 0)
for name, flag := range flags {
result = append(result, map[string]any{
"name": name,
"enabled": flag.Enabled,
})
}
return c.JSON(200, result)
})
provider := feature.NewMemoryProvider()
provider.SetFlag(&feature.Flag{
Name: "new_checkout",
Enabled: true,
Description: "New checkout flow",
Metadata: map[string]any{
"rollout_percentage": 50,
"target_users": "premium",
"experiment_id": "exp-123",
},
})
app.Use(feature.WithProvider(provider))
API Reference
func New(flags Flags) mizu.Middleware
func WithProvider(provider Provider) mizu.Middleware
func WithOptions(opts Options) mizu.Middleware
func GetFlags(c *mizu.Ctx) Flags
func Get(c *mizu.Ctx, name string) *Flag
func IsEnabled(c *mizu.Ctx, name string) bool
func IsDisabled(c *mizu.Ctx, name string) bool
func Require(name string, handler mizu.Handler) mizu.Middleware
func RequireAll(names []string, handler mizu.Handler) mizu.Middleware
func RequireAny(names []string, handler mizu.Handler) mizu.Middleware
// Provider functions
func StaticProvider(flags Flags) Provider
func NewMemoryProvider() *MemoryProvider
Memory Provider Methods
func (p *MemoryProvider) Set(name string, enabled bool)
func (p *MemoryProvider) SetFlag(flag *Flag)
func (p *MemoryProvider) Delete(name string)
func (p *MemoryProvider) Enable(name string)
func (p *MemoryProvider) Disable(name string)
func (p *MemoryProvider) Toggle(name string)
Technical Details
Architecture
The feature middleware uses a provider-based architecture that allows flexible feature flag management:
- Context Storage: Feature flags are stored in the request context using a private
contextKey{} type for isolation
- Provider Interface: The
Provider interface defines a single method GetFlags(c *mizu.Ctx) (Flags, error) enabling custom implementations
- Thread Safety: The
MemoryProvider uses sync.RWMutex for concurrent read/write access
- Immutable Returns: Providers return copies of flag maps to prevent external modification
Implementation Details
Flag Resolution Flow:
- Middleware intercepts request
- Providerβs
GetFlags() is called with context
- Flags are stored in request context
- Helper functions (
IsEnabled, Get, etc.) retrieve flags from context
- On provider error, empty flag map is used (fail-safe behavior)
Static Provider:
- Wraps a
Flags map in an immutable provider
- Returns defensive copies to prevent modification
- Zero overhead after initialization
Memory Provider:
- Mutable in-memory flag storage
- Uses read-write mutex for concurrent access
- Methods:
Enable(), Disable(), Toggle(), Set(), SetFlag(), Delete()
- Safe for runtime flag updates
Middleware Functions:
Require(name, handler): Guards route requiring single flag
RequireAll(names, handler): Guards route requiring all flags enabled
RequireAny(names, handler): Guards route requiring at least one flag enabled
- Custom handlers provide fallback responses when flags are disabled
- Static Provider: O(n) copy operation per request where n is flag count
- Memory Provider: O(n) copy with RLock, write operations use Lock
- Flag Lookup: O(1) map lookup from context
- Context Storage: Single context value, minimal memory overhead
Best Practices
- Use descriptive flag names
- Document flag purposes
- Clean up old flags after rollout
- Use percentage rollouts for risky features
- Implement a custom provider for production
- Include metadata for analytics
Testing
The feature middleware includes comprehensive test coverage for all functionality:
| Test Case | Description | Expected Behavior |
|---|
TestNew | Static flags with enabled/disabled states | Correctly identifies enabled and disabled flags |
TestIsDisabled | Check if flag is disabled | Returns true for disabled flags |
TestGetFlags | Retrieve all flags from context | Returns complete flag collection |
TestGet | Retrieve specific flag with metadata | Returns flag with description and metadata intact |
TestRequire (enabled) | Route protection with enabled flag | Allows request to proceed (200 OK) |
TestRequire (disabled) | Route protection with disabled flag | Blocks request with 404 Not Found |
TestRequire_CustomHandler | Custom handler when flag disabled | Executes custom handler returning 403 Forbidden |
TestRequireAll | Require multiple flags all enabled | Allows request when all flags enabled |
TestRequireAny | Require at least one flag enabled | Allows request when any flag is enabled |
TestMemoryProvider | In-memory provider CRUD operations | Enable, Set, and SetFlag work correctly |
TestMemoryProvider_Toggle | Toggle flag state at runtime | Switches flag from enabled to disabled |
TestMemoryProvider_Delete | Delete flag from provider | Flag no longer exists after deletion |