Skip to Content
Steel is in alpha 🎉
MiddlewareOpinionated Middleware

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.

Last updated on