Middleware
Process requests and responses with powerful middleware system
Middleware in Forge allows you to process HTTP requests and responses in a pipeline, enabling cross-cutting concerns like authentication, logging, CORS, error handling, and more. Forge provides a powerful middleware system that works seamlessly with its handler pattern.
Middleware Pattern
Forge middleware uses a functional pattern with forge.Handler and forge.Context:
type Middleware func(next forge.Handler) forge.Handler
type Handler func(ctx forge.Context) errorThis pattern provides type-safe, composable middleware with proper error handling.
Basic Middleware
Create custom middleware by wrapping handlers:
package main
import (
"time"
"github.com/xraph/forge"
"github.com/xraph/forge/middleware"
)
func main() {
app := forge.New()
// Apply middleware globally
app.Router().Use(
middleware.RequestID(),
middleware.Logging(app.Logger()),
middleware.Recovery(app.Logger()),
middleware.CORS(middleware.DefaultCORSConfig()),
)
app.Router().GET("/users", getUsersHandler)
if err := app.Run(); err != nil {
panic(err)
}
}
// Custom logging middleware
func customLoggingMiddleware(logger forge.Logger) forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
start := time.Now()
logger.Info("request started",
"method", ctx.Request().Method,
"path", ctx.Request().URL.Path,
)
// Call next handler
err := next(ctx)
duration := time.Since(start)
logger.Info("request completed",
"method", ctx.Request().Method,
"path", ctx.Request().URL.Path,
"duration", duration,
)
return err
}
}
}Built-In Middleware
Forge provides production-ready middleware out of the box:
CORS Middleware
Handle Cross-Origin Resource Sharing (CORS) with full configuration support:
import "github.com/xraph/forge/middleware"
// Default CORS configuration (permissive for development)
app.Router().Use(middleware.CORS(middleware.DefaultCORSConfig()))
// Custom CORS configuration
app.Router().Use(middleware.CORS(middleware.CORSConfig{
AllowOrigins: []string{"https://example.com", "https://app.example.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Content-Type", "Authorization", "X-API-Key"},
ExposeHeaders: []string{"X-Request-ID"},
AllowCredentials: true,
MaxAge: 3600, // Cache preflight for 1 hour
}))
// Wildcard subdomain support
app.Router().Use(middleware.CORS(middleware.CORSConfig{
AllowOrigins: []string{"*.example.com"}, // Matches any subdomain
AllowMethods: []string{"GET", "POST"},
}))Features:
- Automatic OPTIONS preflight handling
- Wildcard origin support (
*) - Subdomain wildcard support (
*.example.com) - Credentials handling with security validation
- Proper Vary header management
- Method and header validation
Security Note: AllowOrigins: ["*"] and AllowCredentials: true cannot be used together. The middleware will panic if misconfigured.
Logging Middleware
Structured logging with request/response tracking:
import "github.com/xraph/forge/middleware"
// Basic logging with default configuration
app.Router().Use(middleware.Logging(app.Logger()))
// Custom logging configuration
app.Router().Use(middleware.LoggingWithConfig(
app.Logger(),
middleware.LoggingConfig{
IncludeHeaders: true,
ExcludePaths: []string{"/health", "/metrics"},
SensitiveHeaders: []string{"Authorization", "Cookie"},
},
))Features:
- Request start/completion logging
- Duration tracking
- Path exclusion for health checks
- Sensitive header redaction
- Structured logging support
Default Excluded Paths:
/health/metrics
Default Sensitive Headers:
AuthorizationCookieSet-Cookie
Recovery Middleware
Recover from panics gracefully:
import "github.com/xraph/forge/middleware"
app.Router().Use(middleware.Recovery(app.Logger()))Features:
- Catches panics in handlers
- Logs panic details with stack trace
- Returns
500 Internal Server Error - Prevents server crashes
- Production-ready error handling
// Example handler that panics
app.Router().GET("/panic", func(ctx forge.Context) error {
panic("something went wrong!")
// Recovery middleware catches this and returns 500
})Rate Limiting Middleware
Token bucket algorithm for precise rate limiting:
import "github.com/xraph/forge/middleware"
// Create rate limiter: 100 requests per second, burst of 200
limiter := middleware.NewRateLimiter(100, 200)
// Apply to all routes
app.Router().Use(middleware.RateLimit(limiter, app.Logger()))
// Per-route rate limiting
app.Router().GET("/expensive", handler,
forge.WithMiddleware(middleware.RateLimit(
middleware.NewRateLimiter(10, 20), // Stricter limits
app.Logger(),
)),
)Features:
- Token bucket algorithm (industry standard)
- Per-client rate limiting (by remote address)
- Automatic token refill over time
- Configurable burst capacity
- Background cleanup of old buckets
- Thread-safe implementation
Parameters:
rate: Maximum requests per secondburst: Maximum burst size (bucket capacity)
Response:
- Returns
429 Too Many Requestswhen limit exceeded - Sets
X-Ratelimit-Limit: exceededheader
Request ID Middleware
Track requests with unique identifiers:
import "github.com/xraph/forge/middleware"
app.Router().Use(middleware.RequestID())
// Access request ID in handlers
app.Router().GET("/users", func(ctx forge.Context) error {
requestID := middleware.GetRequestIDFromForgeContext(ctx)
ctx.Logger().Info("processing request", "request_id", requestID)
return ctx.JSON(200, map[string]string{
"request_id": requestID,
})
})Features:
- Generates UUID for each request
- Reuses
X-Request-IDheader if present - Sets response header automatically
- Stores in context for easy access
- Perfect for distributed tracing
Compression Middleware
Gzip compression for responses:
import "github.com/xraph/forge/middleware"
// Default compression level
app.Router().Use(middleware.CompressDefault())
// Custom compression level (1-9)
app.Router().Use(middleware.Compress(6))Features:
- Automatic gzip compression
- Client capability detection
- Configurable compression level
- Response writer wrapping
- Proper header management
Compression Levels:
1: Best speed, least compression6: Default balanced9: Best compression, slower
Timeout Middleware
Enforce request timeouts:
import (
"net/http"
"time"
"github.com/xraph/forge/middleware"
)
// Note: Timeout uses http.Handler pattern
timeoutMiddleware := middleware.Timeout(30*time.Second, app.Logger())
// Apply using PureMiddleware conversion
app.Router().Use(func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
// Create a wrapper that calls the timeout middleware
handler := timeoutMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = next(ctx)
}))
handler.ServeHTTP(ctx.Response(), ctx.Request())
return nil
}
})Features:
- Context-based timeout enforcement
- Safe response buffering
- Race condition prevention
- Returns
504 Gateway Timeout - Goroutine-safe implementation
The Timeout middleware uses http.Handler pattern due to goroutine requirements for proper timeout handling.
Custom Middleware
Authentication Example
Create custom middleware for your application needs:
// JWT Authentication middleware
func jwtAuthMiddleware(secretKey string) forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
authHeader := ctx.Request().Header.Get("Authorization")
if authHeader == "" {
return forge.Unauthorized("authorization header required")
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if tokenString == authHeader {
return forge.Unauthorized("bearer token required")
}
// Validate JWT token (pseudo-code)
claims, err := validateJWT(tokenString, secretKey)
if err != nil {
return forge.Unauthorized("invalid token")
}
// Store claims in context
ctx.Set("user_id", claims.UserID)
ctx.Set("username", claims.Username)
return next(ctx)
}
}
}
// API Key middleware
func apiKeyMiddleware(validKeys map[string]bool) forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
apiKey := ctx.Request().Header.Get("X-API-Key")
if apiKey == "" {
return forge.Unauthorized("API key required")
}
if !validKeys[apiKey] {
return forge.Unauthorized("invalid API key")
}
return next(ctx)
}
}
}Validation Example
import "github.com/go-playground/validator/v10"
// Request validation middleware
func validationMiddleware() forge.Middleware {
validate := validator.New()
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
// Content-Type validation for POST/PUT
if ctx.Request().Method == "POST" || ctx.Request().Method == "PUT" {
contentType := ctx.Request().Header.Get("Content-Type")
if !strings.Contains(contentType, "application/json") {
return forge.BadRequest("content-Type must be application/json")
}
}
// Request size validation
if ctx.Request().ContentLength > 10*1024*1024 { // 10MB
return forge.NewHTTPError(413, "request body too large")
}
return next(ctx)
}
}
}
// Struct validation middleware
func structValidationMiddleware() forge.Middleware {
validate := validator.New()
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
// This would be applied after BindJSON
// Handler should validate the struct manually
return next(ctx)
}
}
}Middleware Chaining
Chain multiple middleware in a specific order:
import "github.com/xraph/forge/middleware"
func setupMiddlewareChain(app *forge.App) {
// Global middleware (applied to all routes)
// Order matters: request ID → recovery → logging → CORS
app.Router().Use(
middleware.RequestID(),
middleware.Recovery(app.Logger()),
middleware.Logging(app.Logger()),
middleware.CORS(middleware.DefaultCORSConfig()),
)
// API routes with rate limiting and auth
limiter := middleware.NewRateLimiter(100, 200)
api := app.Router().Group("/api",
forge.WithGroupMiddleware(middleware.RateLimit(limiter, app.Logger())),
forge.WithGroupMiddleware(jwtAuthMiddleware("secret")),
)
// Admin routes with stricter limits
adminLimiter := middleware.NewRateLimiter(10, 20)
admin := api.Group("/admin",
forge.WithGroupMiddleware(middleware.RateLimit(adminLimiter, app.Logger())),
forge.WithGroupMiddleware(requireAdminRole()),
forge.WithGroupMiddleware(auditLogMiddleware()),
)
admin.GET("/users", adminGetUsersHandler)
}
// Admin role validation
func requireAdminRole() forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
userRole := ctx.Get("user_role")
if userRole != "admin" {
return forge.Forbidden("admin access required")
}
return next(ctx)
}
}
}
// Audit logging middleware
func auditLogMiddleware() forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
userID := ctx.Get("user_id")
ctx.Logger().Info("admin action",
"user_id", userID,
"path", ctx.Request().URL.Path,
"method", ctx.Request().Method,
)
return next(ctx)
}
}
}Middleware Order Matters
The order of middleware execution is important:
- Request ID - Generate tracking ID first
- Recovery - Catch panics early
- Logging - Log with request ID
- CORS - Handle CORS before auth
- Authentication - Verify identity
- Authorization - Check permissions
- Rate Limiting - After auth for accurate limiting
Route-Specific Middleware
Apply middleware to individual routes:
import "github.com/xraph/forge/middleware"
func setupRouteMiddleware(app *forge.App) {
// Public route (no middleware)
app.Router().GET("/public/data", getPublicDataHandler)
// Route with authentication
app.Router().GET("/private/data", getPrivateDataHandler,
forge.WithMiddleware(jwtAuthMiddleware("secret")),
)
// Route with multiple middleware
app.Router().POST("/upload", uploadHandler,
forge.WithMiddleware(jwtAuthMiddleware("secret")),
forge.WithMiddleware(fileSizeValidation(50*1024*1024)), // 50MB
forge.WithMiddleware(fileTypeValidation([]string{".jpg", ".png"})),
)
// Admin route with strict rate limit
adminLimiter := middleware.NewRateLimiter(10, 20)
app.Router().GET("/admin/stats", getStatsHandler,
forge.WithMiddleware(jwtAuthMiddleware("secret")),
forge.WithMiddleware(requireAdminRole()),
forge.WithMiddleware(middleware.RateLimit(adminLimiter, app.Logger())),
)
}
// File size validation middleware
func fileSizeValidation(maxSize int64) forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
if ctx.Request().ContentLength > maxSize {
return forge.NewHTTPError(413, "file too large")
}
return next(ctx)
}
}
}
// File type validation middleware
func fileTypeValidation(allowedTypes []string) forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
contentType := ctx.Request().Header.Get("Content-Type")
allowed := false
for _, t := range allowedTypes {
if strings.Contains(contentType, t) {
allowed = true
break
}
}
if !allowed {
return forge.BadRequest("invalid file type")
}
return next(ctx)
}
}
}Advanced Middleware Patterns
Conditional Middleware
Execute middleware based on conditions:
// Conditional middleware factory
func conditionalMiddleware(condition bool, middleware forge.Middleware) forge.Middleware {
if condition {
return middleware
}
// No-op middleware
return func(next forge.Handler) forge.Handler {
return next
}
}
func setupConditionalMiddleware(app *forge.App, config *AppConfig) {
app.Router().Use(
conditionalMiddleware(config.Debug, debugMiddleware()),
conditionalMiddleware(config.EnableMetrics, metricsMiddleware()),
conditionalMiddleware(config.EnableTracing, tracingMiddleware()),
)
}
// Environment-based middleware
func envMiddleware(env string) forge.Middleware {
if env == "development" {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
// Add debug headers
ctx.SetHeader("X-Environment", "development")
ctx.SetHeader("X-Debug", "true")
return next(ctx)
}
}
}
return func(next forge.Handler) forge.Handler {
return next
}
}Middleware with Dependencies
Inject dependencies into middleware:
import "github.com/xraph/forge/middleware"
// Middleware factory with dependencies
type MiddlewareFactory struct {
logger forge.Logger
config *AppConfig
cache CacheService
metrics MetricsService
}
func NewMiddlewareFactory(
logger forge.Logger,
config *AppConfig,
cache CacheService,
metrics MetricsService,
) *MiddlewareFactory {
return &MiddlewareFactory{
logger: logger,
config: config,
cache: cache,
metrics: metrics,
}
}
// Create middleware with injected dependencies
func (mf *MiddlewareFactory) CachingMiddleware(ttl time.Duration) forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
// Check cache
cacheKey := ctx.Request().URL.Path
if cached, found := mf.cache.Get(cacheKey); found {
mf.metrics.IncrementCacheHits()
return ctx.JSON(200, cached)
}
// Process request
err := next(ctx)
// Cache response (simplified)
mf.metrics.IncrementCacheMisses()
return err
}
}
}
// Metrics middleware
func (mf *MiddlewareFactory) MetricsMiddleware() forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
start := time.Now()
err := next(ctx)
duration := time.Since(start)
mf.metrics.RecordRequest(
ctx.Request().Method,
ctx.Request().URL.Path,
duration,
)
return err
}
}
}Configurable Middleware
Create reusable configurable middleware:
// Cache configuration
type CacheConfig struct {
TTL time.Duration
KeyPrefix string
OnlyMethods []string
}
// Caching middleware with config
func cachingMiddleware(cache CacheService, config CacheConfig) forge.Middleware {
// Pre-compile method check map
allowedMethods := make(map[string]bool)
for _, method := range config.OnlyMethods {
allowedMethods[method] = true
}
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
method := ctx.Request().Method
// Skip if method not allowed
if len(allowedMethods) > 0 && !allowedMethods[method] {
return next(ctx)
}
cacheKey := config.KeyPrefix + ctx.Request().URL.Path
// Check cache
if cached, found := cache.Get(cacheKey); found {
return ctx.JSON(200, cached)
}
// Execute handler and cache result
err := next(ctx)
// Cache logic here
return err
}
}
}Error Handling in Middleware
Forge's error handling system integrates seamlessly with middleware:
import "github.com/xraph/forge/errors"
// Error wrapping middleware
func errorContextMiddleware() forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
err := next(ctx)
if err != nil {
// Add context to errors
requestID := middleware.GetRequestIDFromForgeContext(ctx)
ctx.Logger().Error("request failed",
"error", err,
"request_id", requestID,
"path", ctx.Request().URL.Path,
"method", ctx.Request().Method,
)
// Return the error for the error handler
return err
}
return nil
}
}
}
// Validation error middleware
func validationMiddleware(validator *validator.Validate) forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
// Handler will validate and return errors
err := next(ctx)
// Transform validation errors
if verr, ok := err.(validator.ValidationErrors); ok {
details := make(map[string]string)
for _, fe := range verr {
details[fe.Field()] = fe.Tag()
}
return forge.NewHTTPError(422, "validation failed", details)
}
return err
}
}
}
// Database error transformation
func dbErrorMiddleware() forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
err := next(ctx)
if err != nil {
// Transform database errors
if isDuplicateKeyError(err) {
return forge.NewHTTPError(409, "resource already exists")
}
if isNotFoundError(err) {
return forge.NotFound("resource not found")
}
}
return err
}
}
}Error Flow in Forge
- Handler returns error
- Middleware can wrap or transform errors
- Error handler converts to HTTP response
- Recovery middleware catches panics
Use forge.HTTPError for structured error responses.
Performance Optimization
Optimize middleware for production workloads:
Pre-compilation
Pre-compile expensive operations during initialization:
// Pre-compile regex patterns
func pathMatchingMiddleware() forge.Middleware {
// Compile once at initialization
apiPattern := regexp.MustCompile(`^/api/v\d+/`)
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
path := ctx.Request().URL.Path
// Fast path check
if !apiPattern.MatchString(path) {
return next(ctx)
}
// Expensive operations only for matching paths
// ... middleware logic
return next(ctx)
}
}
}Object Pooling
Use sync.Pool for frequently allocated objects:
import "sync"
// Buffer pool for reading bodies
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 4096)
},
}
func bufferedMiddleware() forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
// Get buffer from pool
buffer := bufferPool.Get().([]byte)
defer bufferPool.Put(buffer)
// Use buffer for operations
// ... middleware logic
return next(ctx)
}
}
}Early Returns
Exit middleware early when possible:
func conditionalMiddleware() forge.Middleware {
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
// Fast path: skip middleware for certain routes
if strings.HasPrefix(ctx.Request().URL.Path, "/health") {
return next(ctx)
}
// Only do expensive work when needed
if ctx.Request().Method == "POST" {
// Expensive validation
}
return next(ctx)
}
}
}Avoid Allocations
Minimize memory allocations in hot paths:
func lowAllocMiddleware() forge.Middleware {
// Pre-allocate reusable structures
type contextData struct {
StartTime time.Time
RequestID string
}
return func(next forge.Handler) forge.Handler {
return func(ctx forge.Context) error {
// Reuse context values instead of creating new ones
start := time.Now()
err := next(ctx)
// Minimize string concatenation
_ = time.Since(start)
return err
}
}
}Best Practices
Middleware Best Practices
- Order Matters: Request ID → Recovery → Logging → CORS → Auth → Business Logic
- Error Handling: Return
forge.HTTPErrorfor proper error responses - Performance: Pre-compile patterns, use object pools, exit early
- Context Usage: Use
ctx.Set()andctx.Get()for request-scoped data - Testing: Write table-driven tests for all middleware
- Reusability: Create configurable factories for common patterns
- Security: Validate all inputs, never log sensitive data
- Observability: Always include request ID in logs
Testing Middleware
Write comprehensive tests for your middleware:
package middleware_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/xraph/forge"
"github.com/xraph/forge/testing"
)
func TestAuthMiddleware(t *testing.T) {
tests := []struct {
name string
authHeader string
expectedStatus int
expectedError bool
}{
{
name: "valid token",
authHeader: "Bearer valid-token",
expectedStatus: 200,
expectedError: false,
},
{
name: "missing token",
authHeader: "",
expectedStatus: 401,
expectedError: true,
},
{
name: "invalid token",
authHeader: "Bearer invalid",
expectedStatus: 401,
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create test handler
handler := func(ctx forge.Context) error {
return ctx.JSON(200, map[string]string{"status": "ok"})
}
// Wrap with middleware
middleware := jwtAuthMiddleware("test-secret")
wrappedHandler := middleware(handler)
// Create test request
req := httptest.NewRequest("GET", "/test", nil)
if tt.authHeader != "" {
req.Header.Set("Authorization", tt.authHeader)
}
// Create test context
rec := httptest.NewRecorder()
ctx := forgetesting.NewTestContext(rec, req)
// Execute handler
err := wrappedHandler(ctx)
// Assertions
if tt.expectedError && err == nil {
t.Error("expected error, got nil")
}
if !tt.expectedError && err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}
func TestLoggingMiddleware(t *testing.T) {
// Mock logger
var loggedMessages []string
mockLogger := &MockLogger{
InfoFunc: func(msg string, args ...interface{}) {
loggedMessages = append(loggedMessages, msg)
},
}
// Create middleware
middleware := customLoggingMiddleware(mockLogger)
handler := func(ctx forge.Context) error {
return ctx.JSON(200, map[string]string{"status": "ok"})
}
// Test
req := httptest.NewRequest("GET", "/test", nil)
rec := httptest.NewRecorder()
ctx := forgetesting.NewTestContext(rec, req)
wrappedHandler := middleware(handler)
_ = wrappedHandler(ctx)
// Verify logging
if len(loggedMessages) < 2 {
t.Error("expected at least 2 log messages")
}
}Middleware Reference
Built-In Middleware Summary
| Middleware | Purpose | Import |
|---|---|---|
CORS | Cross-origin resource sharing | middleware.CORS() |
Logging | Request/response logging | middleware.Logging() |
Recovery | Panic recovery | middleware.Recovery() |
RateLimit | Token bucket rate limiting | middleware.RateLimit() |
RequestID | Unique request tracking | middleware.RequestID() |
Compress | Gzip compression | middleware.Compress() |
Timeout | Request timeout enforcement | middleware.Timeout() |
Common Middleware Patterns
import "github.com/xraph/forge/middleware"
// Production middleware stack
app.Router().Use(
middleware.RequestID(), // 1. Generate request ID
middleware.Recovery(app.Logger()), // 2. Catch panics
middleware.Logging(app.Logger()), // 3. Log requests
middleware.CORS(middleware.DefaultCORSConfig()), // 4. Handle CORS
middleware.CompressDefault(), // 5. Compress responses
)
// API group with auth and rate limiting
limiter := middleware.NewRateLimiter(100, 200)
api := app.Router().Group("/api",
forge.WithGroupMiddleware(middleware.RateLimit(limiter, app.Logger())),
forge.WithGroupMiddleware(jwtAuthMiddleware("secret")),
)
// Admin group with strict limits
adminLimiter := middleware.NewRateLimiter(10, 20)
admin := api.Group("/admin",
forge.WithGroupMiddleware(middleware.RateLimit(adminLimiter, app.Logger())),
forge.WithGroupMiddleware(requireAdminRole()),
)Next Steps
- Learn about Route Parameters for dynamic routing
- Explore Controllers for organizing route logic
- See Route Groups for organizing routes with shared middleware
- Check out Error Handling for the error system
- Review Context API for request context usage
How is this guide?
Last updated on