Opinionated Middleware
Opinionated middleware in Steel provides enhanced functionality beyond standard HTTP middleware. It integrates with OpenAPI documentation, provides rich context information, and supports advanced patterns like dependency validation and automatic security requirements.
Understanding Opinionated Middleware
While standard middleware operates on http.Handler functions, opinionated middleware works with Steel’s enhanced context and automatically contributes to OpenAPI documentation.
Key Features
- OpenAPI Integration: Automatically documents security requirements, headers, and responses
- Rich Context: Access to handler metadata, input/output types, and request state
- Dependency Management: Validates middleware dependencies and conflicts
- Type Safety: Works with strongly-typed handlers and parameters
- Middleware Chain Validation: Ensures proper middleware ordering and compatibility
Basic Opinionated Middleware
Creating Middleware with the Builder
import "github.com/xraph/steel"
// Create a simple request logging middleware
func RequestLoggingMiddleware() steel.OpinionatedMiddleware {
return steel.NewMiddleware("request_logging").
Description("Logs incoming requests with timing information").
Before(func(ctx *steel.MiddlewareContext) error {
ctx.StartTime = time.Now()
log.Printf("Request started: %s %s", ctx.Request.Method, ctx.Request.URL.Path)
return nil
}).
After(func(ctx *steel.MiddlewareContext) error {
duration := time.Since(ctx.StartTime)
status := ctx.StatusCode
if status == 0 {
status = 200 // Default status
}
log.Printf("Request completed: %s %s %d %v",
ctx.Request.Method, ctx.Request.URL.Path, status, duration)
return nil
}).
CachingSafe().
Build()
}
// Use the middleware
router := steel.NewRouter()
router.UseOpinionated(RequestLoggingMiddleware())Middleware Context
The MiddlewareContext provides rich information about the request and handler:
func ExampleMiddleware() steel.OpinionatedMiddleware {
return steel.NewMiddleware("example").
Before(func(ctx *steel.MiddlewareContext) error {
// Request information
method := ctx.Request.Method
path := ctx.Request.URL.Path
userAgent := ctx.Request.UserAgent()
// Handler information (if available)
if ctx.HandlerInfo != nil {
handlerPath := ctx.HandlerInfo.Path
inputType := ctx.HandlerInfo.InputType
tags := ctx.HandlerInfo.Tags
log.Printf("Calling handler %s with input type %v, tags: %v",
handlerPath, inputType, tags)
}
// Set metadata for use in other middleware or handlers
ctx.Metadata["start_time"] = time.Now()
ctx.Metadata["user_agent"] = userAgent
// Set request ID if not present
if ctx.RequestID == "" {
ctx.RequestID = generateRequestID()
ctx.Headers["X-Request-ID"] = ctx.RequestID
}
return nil
}).
Build()
}OpenAPI Integration
Opinionated middleware automatically contributes to OpenAPI documentation:
Security Middleware
func JWTAuthMiddleware(secretKey string) steel.OpinionatedMiddleware {
return steel.NewMiddleware("jwt_auth").
Description("JWT Bearer token authentication").
Before(func(ctx *steel.MiddlewareContext) error {
authHeader := ctx.Request.Header.Get("Authorization")
if authHeader == "" {
return steel.Unauthorized("Authorization header required")
}
if !strings.HasPrefix(authHeader, "Bearer ") {
return steel.Unauthorized("Bearer token required")
}
token := authHeader[7:]
claims, err := validateJWTToken(token, secretKey)
if err != nil {
return steel.Unauthorized("Invalid token")
}
// Store user information in context
ctx.UserID = claims.UserID
ctx.Metadata["user_claims"] = claims
return nil
}).
RequiresAuth().
AddSecurityRequirement(steel.RequireBearer("JWTAuth")).
AddResponse("401", "Unauthorized - Invalid or missing JWT token").
AddHeader("Authorization", "JWT Bearer token", true).
Build()
}
// Register the security scheme
router.RegisterSecurityScheme("JWTAuth", steel.BearerAuth(
"JWT Bearer token authentication",
"JWT",
))CORS Middleware with OpenAPI Documentation
func CORSMiddleware(config CORSConfig) steel.OpinionatedMiddleware {
return steel.NewMiddleware("cors").
Description("Cross-Origin Resource Sharing (CORS) middleware").
Before(func(ctx *steel.MiddlewareContext) error {
origin := ctx.Request.Header.Get("Origin")
// Check if origin is allowed
allowOrigin := ""
for _, allowedOrigin := range config.AllowedOrigins {
if allowedOrigin == "*" || allowedOrigin == origin {
allowOrigin = allowedOrigin
break
}
}
if allowOrigin != "" {
ctx.Headers["Access-Control-Allow-Origin"] = allowOrigin
}
if config.AllowCredentials {
ctx.Headers["Access-Control-Allow-Credentials"] = "true"
}
// Handle preflight requests
if ctx.Request.Method == "OPTIONS" {
ctx.Headers["Access-Control-Allow-Methods"] = strings.Join(config.AllowedMethods, ", ")
ctx.Headers["Access-Control-Allow-Headers"] = strings.Join(config.AllowedHeaders, ", ")
ctx.Headers["Access-Control-Max-Age"] = "86400"
ctx.StatusCode = http.StatusNoContent
ctx.Processed = true
return nil
}
return nil
}).
CachingSafe().
AddHeader("Access-Control-Allow-Origin", "Allowed origins for CORS", false).
AddHeader("Access-Control-Allow-Methods", "Allowed methods for CORS", false).
AddHeader("Access-Control-Allow-Headers", "Allowed headers for CORS", false).
Build()
}
type CORSConfig struct {
AllowedOrigins []string
AllowedMethods []string
AllowedHeaders []string
AllowCredentials bool
}Rate Limiting with Metadata
func RateLimitMiddleware(config RateLimitConfig) steel.OpinionatedMiddleware {
store := NewInMemoryRateLimitStore(config.RequestsPerSecond, config.BurstSize)
return steel.NewMiddleware("rate_limit").
Description("Rate limiting middleware to prevent abuse").
Before(func(ctx *steel.MiddlewareContext) error {
// Get client identifier
clientID := getClientID(ctx.Request)
limiter := store.GetLimiter(clientID)
if !limiter.Allow() {
// Add rate limit headers
ctx.Headers["X-RateLimit-Limit"] = fmt.Sprintf("%.0f", config.RequestsPerSecond)
ctx.Headers["X-RateLimit-Remaining"] = "0"
ctx.Headers["X-RateLimit-Reset"] = fmt.Sprintf("%d", time.Now().Add(time.Second).Unix())
return steel.TooManyRequests("Rate limit exceeded")
}
// Add rate limit info to successful requests
ctx.Headers["X-RateLimit-Limit"] = fmt.Sprintf("%.0f", config.RequestsPerSecond)
return nil
}).
AddResponse("429", "Rate limit exceeded").
AddHeader("X-RateLimit-Limit", "Request rate limit", false).
AddHeader("X-RateLimit-Remaining", "Remaining requests", false).
AddHeader("X-RateLimit-Reset", "Rate limit reset time", false).
Build()
}
type RateLimitConfig struct {
RequestsPerSecond float64
BurstSize int
}Advanced Middleware Patterns
Typed Middleware
Create middleware that works with specific input/output types:
// User authentication middleware that works with user-related handlers
func UserContextMiddleware[TInput any, TOutput any]() steel.TypedMiddleware[TInput, TOutput] {
return &userContextMiddleware[TInput, TOutput]{}
}
type userContextMiddleware[TInput any, TOutput any] struct{}
func (m *userContextMiddleware[TInput, TOutput]) Process(
ctx *steel.Context,
input *TInput,
output *TOutput,
next steel.TypedNext[TInput, TOutput],
) error {
// Extract user information from JWT token
userID := getUserIDFromToken(ctx.Request)
if userID == "" {
return steel.Unauthorized("User authentication required")
}
// Load user from database
user, err := loadUser(userID)
if err != nil {
return steel.InternalServerError("Failed to load user")
}
// Add user to request context
userCtx := context.WithValue(ctx.Request.Context(), "user", user)
ctx.Request = ctx.Request.WithContext(userCtx)
// Call next handler
return next(ctx, input)
}
func (m *userContextMiddleware[TInput, TOutput]) GetMetadata() steel.MiddlewareMetadata {
return steel.MiddlewareMetadata{
Name: "user_context",
Description: "Loads user context from authentication token",
RequiresAuth: true,
}
}Dependency Management
Define middleware dependencies and conflicts:
func DatabaseTransactionMiddleware() steel.OpinionatedMiddleware {
return steel.NewMiddleware("db_transaction").
Description("Manages database transactions for write operations").
DependsOn("request_id"). // Requires request ID for logging
ConflictsWith("read_only"). // Cannot be used with read-only middleware
Before(func(ctx *steel.MiddlewareContext) error {
// Only use transactions for write operations
if ctx.Request.Method == "GET" || ctx.Request.Method == "HEAD" {
return nil
}
tx, err := db.Begin()
if err != nil {
return steel.InternalServerError("Failed to start transaction")
}
ctx.Metadata["db_transaction"] = tx
return nil
}).
After(func(ctx *steel.MiddlewareContext) error {
tx, ok := ctx.Metadata["db_transaction"].(*sql.Tx)
if !ok || tx == nil {
return nil
}
// Commit or rollback based on response status
if ctx.Error != nil || ctx.StatusCode >= 400 {
tx.Rollback()
} else {
if err := tx.Commit(); err != nil {
log.Printf("Failed to commit transaction: %v", err)
return steel.InternalServerError("Transaction commit failed")
}
}
return nil
}).
ModifiesRequest().
Build()
}Error Handling Middleware
func ErrorHandlingMiddleware() steel.OpinionatedMiddleware {
return steel.NewMiddleware("error_handling").
Description("Centralized error handling and logging").
OnError(func(ctx *steel.MiddlewareContext, err error) error {
// Log the error
log.Printf("Request error [%s]: %s %s - %v",
ctx.RequestID, ctx.Request.Method, ctx.Request.URL.Path, err)
// Add error context
if ctx.RequestID != "" {
ctx.Headers["X-Request-ID"] = ctx.RequestID
}
// Convert to API error if needed
if apiErr, ok := err.(steel.APIError); ok {
return apiErr
}
// Default to internal server error
return steel.InternalServerError("An unexpected error occurred")
}).
ModifiesResponse().
Build()
}Conditional Middleware
Apply middleware conditionally based on request properties:
func ConditionalAuthMiddleware() steel.OpinionatedMiddleware {
return steel.NewMiddleware("conditional_auth").
Description("Authentication required for non-public endpoints").
Before(func(ctx *steel.MiddlewareContext) error {
// Skip authentication for public endpoints
if isPublicEndpoint(ctx.Request.URL.Path) {
return nil
}
// Skip authentication for OPTIONS requests (CORS preflight)
if ctx.Request.Method == "OPTIONS" {
return nil
}
// Require authentication for all other requests
token := ctx.Request.Header.Get("Authorization")
if token == "" {
return steel.Unauthorized("Authentication required")
}
// Validate token
claims, err := validateToken(token)
if err != nil {
return steel.Unauthorized("Invalid token")
}
ctx.UserID = claims.UserID
ctx.Metadata["user_claims"] = claims
return nil
}).
RequiresAuth().
Build()
}
func isPublicEndpoint(path string) bool {
publicPaths := []string{
"/health",
"/metrics",
"/api/v1/auth/login",
"/api/v1/auth/register",
}
for _, publicPath := range publicPaths {
if path == publicPath {
return true
}
}
return false
}Using Opinionated Middleware
Global Middleware
Apply middleware to all opinionated handlers:
router := steel.NewRouter()
// Global opinionated middleware
router.UseOpinionated(
RequestIDMiddleware(),
ErrorHandlingMiddleware(),
CORSMiddleware(CORSConfig{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
}),
)
// Conditional middleware
router.UseOpinionatedIf(config.EnableRateLimit, RateLimitMiddleware(RateLimitConfig{
RequestsPerSecond: 100,
BurstSize: 200,
}))Route Group Middleware
Apply middleware to specific route groups:
router.Route("/api/v1", func(api router.Router) {
// API-specific middleware
api.UseOpinionated(
APIVersionMiddleware("v1"),
RequestLoggingMiddleware(),
)
// Public routes
api.OpinionatedPOST("/auth/login", loginHandler)
// Protected routes
api.Route("/protected", func(protected router.Router) {
protected.UseOpinionated(
JWTAuthMiddleware(config.JWTSecret),
UserContextMiddleware(),
)
protected.OpinionatedGET("/profile", getProfileHandler)
protected.OpinionatedPUT("/profile", updateProfileHandler)
})
})Middleware Validation
Validate middleware configuration at startup:
func main() {
router := steel.NewRouter()
// Add middleware
router.UseOpinionated(
RequestIDMiddleware(),
JWTAuthMiddleware(config.JWTSecret),
DatabaseTransactionMiddleware(),
ErrorHandlingMiddleware(),
)
// Validate middleware chain
if err := router.ValidateMiddleware(); err != nil {
log.Fatalf("Middleware validation failed: %v", err)
}
// Print middleware info for debugging
router.PrintMiddlewareInfo()
// Start server
log.Fatal(http.ListenAndServe(":8080", router))
}Testing Opinionated Middleware
Test middleware with enhanced context:
func TestJWTAuthMiddleware(t *testing.T) {
middleware := JWTAuthMiddleware("test-secret")
tests := []struct {
name string
authHeader string
expectedError bool
expectedUserID string
}{
{
name: "Valid token",
authHeader: "Bearer " + generateTestToken("user123"),
expectedError: false,
expectedUserID: "user123",
},
{
name: "Missing token",
authHeader: "",
expectedError: true,
},
{
name: "Invalid token",
authHeader: "Bearer invalid-token",
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create test context
req := httptest.NewRequest("GET", "/test", nil)
if tt.authHeader != "" {
req.Header.Set("Authorization", tt.authHeader)
}
ctx := &steel.MiddlewareContext{
Context: &steel.Context{
Request: req,
Response: httptest.NewRecorder(),
},
Metadata: make(map[string]interface{}),
Headers: make(map[string]string),
}
// Execute middleware
err := middleware.Process(ctx, func() error {
return nil // Mock next function
})
// Validate results
if tt.expectedError && err == nil {
t.Error("Expected error but got none")
}
if !tt.expectedError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
if tt.expectedUserID != "" && ctx.UserID != tt.expectedUserID {
t.Errorf("Expected user ID %s, got %s", tt.expectedUserID, ctx.UserID)
}
})
}
}Performance Considerations
Performance Tip: Opinionated middleware adds slight overhead for OpenAPI integration. Use regular middleware for performance-critical paths that don’t need documentation.
Efficient Middleware Design
// âś… Good: Efficient middleware with early returns
func EfficientAuthMiddleware() steel.OpinionatedMiddleware {
return steel.NewMiddleware("efficient_auth").
Before(func(ctx *steel.MiddlewareContext) error {
// Quick checks first
if ctx.Request.Method == "OPTIONS" {
return nil // Skip auth for preflight
}
authHeader := ctx.Request.Header.Get("Authorization")
if authHeader == "" {
return steel.Unauthorized("Authorization required")
}
// Validate token (cached validation would be even better)
claims, err := validateTokenCached(authHeader)
if err != nil {
return steel.Unauthorized("Invalid token")
}
ctx.UserID = claims.UserID
return nil
}).
Build()
}Opinionated middleware in Steel provides a powerful way to add functionality while automatically contributing to API documentation and maintaining type safety.