Middleware

Add cross-cutting concerns to your routes with middleware

Middleware in Forge wraps handlers to add cross-cutting concerns such as logging, authentication, compression, and rate limiting. Middleware executes before and after the handler, forming a chain around the request lifecycle.

Middleware Signature

A Forge middleware is a function that takes the next handler and returns a new handler that wraps it.

type Middleware func(next forge.Handler) Handler

This follows the standard decorator pattern. Each middleware receives the next handler in the chain and decides when (or if) to call it.

func TimingMiddleware() forge.Middleware {
    return func(next forge.Handler) forge.Handler {
        return func(ctx forge.Context) error {
            start := time.Now()

            // Call the next handler
            err := next(ctx)

            duration := time.Since(start)
            fmt.Printf("Request took %s\n", duration)

            return err
        }
    }
}

Applying Middleware

Forge provides three levels for applying middleware: global, group, and route.

Global Middleware

Applied to every route in the application. Use UseGlobal to register global middleware. Global middleware runs outermost in the chain, before group and route middleware.

app.UseGlobal(
    middleware.Recovery(logger),
    middleware.RequestID(),
    middleware.CORS(middleware.DefaultCORSConfig()),
)

Group Middleware

Applied to all routes within a group. Use Use on the router or WithGroupMiddleware when creating a group.

api := app.Group("/api", forge.WithGroupMiddleware(
    middleware.Logging(logger),
))
api.Use(middleware.CompressDefault())

Route Middleware

Applied to a single route via WithMiddleware.

app.POST("/upload", uploadHandler,
    forge.WithMiddleware(middleware.RateLimit(limiter, logger)),
)

Execution Order

Middleware executes in the order it is registered, with global middleware outermost and route middleware innermost.

For a request to POST /api/v1/upload:

Global middleware (Recovery -> RequestID -> CORS)
  -> Group middleware (Logging)
    -> Route middleware (RateLimit)
      -> Handler
    -> Route middleware (after)
  -> Group middleware (after)
Global middleware (after)

Built-in Middleware

Forge ships with production-ready middleware in the middleware package.

CORS

Handles Cross-Origin Resource Sharing with full preflight support, origin validation, and wildcard subdomain matching.

import "github.com/xraph/forge/middleware"

// Development: allow all origins
app.Use(middleware.CORS(middleware.DefaultCORSConfig()))

// Production: restrict origins
app.Use(middleware.CORS(middleware.CORSConfig{
    AllowOrigins:     []string{"https://app.example.com", "https://*.example.com"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowHeaders:     []string{"Content-Type", "Authorization", "X-Request-ID"},
    ExposeHeaders:    []string{"X-Request-ID", "X-RateLimit-Remaining"},
    AllowCredentials: true,
    MaxAge:           86400,
}))

AllowCredentials cannot be true when AllowOrigins contains "*". The CORS middleware panics at initialization if this misconfiguration is detected.

The CORSConfig struct fields:

FieldTypeDescription
AllowOrigins[]stringAllowed origins. Use * for all, or specific URLs. Supports *.example.com wildcards.
AllowMethods[]stringAllowed HTTP methods
AllowHeaders[]stringAllowed request headers
ExposeHeaders[]stringHeaders exposed to the client
AllowCredentialsboolWhether to allow credentials
MaxAgeintPreflight cache duration in seconds

Recovery

Recovers from panics in handlers and returns a 500 Internal Server Error. Logs the panic with a stack trace.

app.Use(middleware.Recovery(logger))

In production, this prevents a single panicking request from crashing the entire server process.

Logging

Logs HTTP requests with timing information. Supports path exclusions and sensitive header redaction.

// Basic logging
app.Use(middleware.Logging(logger))

// Custom configuration
app.Use(middleware.LoggingWithConfig(logger, middleware.LoggingConfig{
    IncludeHeaders:   true,
    ExcludePaths:     []string{"/health", "/metrics", "/readiness"},
    SensitiveHeaders: []string{"Authorization", "Cookie", "Set-Cookie", "X-API-Key"},
}))

The default configuration excludes /health and /metrics paths and redacts Authorization, Cookie, and Set-Cookie headers.

Compression

Compresses HTTP responses using gzip when the client supports it.

import "compress/gzip"

// Default compression level
app.Use(middleware.CompressDefault())

// Custom compression level
app.Use(middleware.Compress(gzip.BestSpeed))

The middleware checks the Accept-Encoding header and only compresses when the client includes gzip. It sets the appropriate Content-Encoding and Vary response headers.

RateLimit

Enforces rate limiting using a token bucket algorithm. Rate limiting is per-client based on the remote address.

// Allow 100 requests per second with a burst of 200
limiter := middleware.NewRateLimiter(100, 200)
app.Use(middleware.RateLimit(limiter, logger))

The RateLimiter automatically cleans up stale buckets every 5 minutes. When a client exceeds the limit, the middleware returns 429 Too Many Requests.

// Create a rate limiter for a specific route
uploadLimiter := middleware.NewRateLimiter(10, 20) // 10 req/s, burst 20
app.POST("/upload", uploadHandler,
    forge.WithMiddleware(middleware.RateLimit(uploadLimiter, logger)),
)

RequestID

Adds a unique request ID to every request. If the incoming request has an X-Request-ID header, it is reused. Otherwise, a new UUID is generated.

app.Use(middleware.RequestID())

The request ID is available in the context and set as a response header:

app.GET("/debug", func(ctx forge.Context) error {
    requestID := middleware.GetRequestIDFromForgeContext(ctx)
    return ctx.JSON(200, map[string]string{"request_id": requestID})
})

Timeout

Enforces a timeout on request handling. If the handler does not complete within the specified duration, the client receives a 504 Gateway Timeout.

app.Use(middleware.Timeout(30*time.Second, logger))

The Timeout middleware uses the http.Handler pattern internally due to goroutine requirements. It wraps the response writer to prevent race conditions between the handler goroutine and the timeout.

Custom Middleware

Building custom middleware follows the same pattern. Here is a middleware that adds security headers:

func SecurityHeaders() forge.Middleware {
    return func(next forge.Handler) forge.Handler {
        return func(ctx forge.Context) error {
            ctx.SetHeader("X-Content-Type-Options", "nosniff")
            ctx.SetHeader("X-Frame-Options", "DENY")
            ctx.SetHeader("X-XSS-Protection", "1; mode=block")
            ctx.SetHeader("Strict-Transport-Security", "max-age=63072000; includeSubDomains")

            return next(ctx)
        }
    }
}

A middleware that short-circuits the chain (does not call next):

func MaintenanceMode(enabled *atomic.Bool) forge.Middleware {
    return func(next forge.Handler) forge.Handler {
        return func(ctx forge.Context) error {
            if enabled.Load() {
                return ctx.JSON(503, map[string]string{
                    "error": "Service under maintenance",
                })
            }
            return next(ctx)
        }
    }
}

Chain Utility

Combine multiple middleware into a single middleware using Chain. Middleware are applied in the order provided -- the first middleware in the list wraps the outermost layer.

import "github.com/xraph/forge/internal/router"

securityStack := router.Chain(
    middleware.Recovery(logger),
    middleware.RequestID(),
    middleware.CORS(corsConfig),
    middleware.Logging(logger),
)

app.Use(securityStack)

This is equivalent to calling app.Use with each middleware individually:

app.Use(middleware.Recovery(logger))
app.Use(middleware.RequestID())
app.Use(middleware.CORS(corsConfig))
app.Use(middleware.Logging(logger))

Converting http.Handler Middleware

If you have existing middleware written for net/http (the func(http.Handler) http.Handler pattern), convert it to a Forge middleware with PureMiddleware.ToMiddleware().

// Existing http.Handler middleware
func ThirdPartyMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // pre-processing
        next.ServeHTTP(w, r)
        // post-processing
    })
}

// Convert to forge.Middleware
forgeMiddleware := forge.PureMiddleware(ThirdPartyMiddleware).ToMiddleware()
app.Use(forgeMiddleware)

This enables you to reuse the large ecosystem of existing Go HTTP middleware with Forge applications.

Use vs UseGlobal

Both Use and UseGlobal register middleware, but they differ in scope and ordering:

MethodScopeExecution Order
UseGlobalAll routes in the applicationOutermost -- runs first on every request
UseRoutes on the current router/groupRuns after global middleware
WithMiddlewareSingle routeInnermost -- runs closest to the handler
// Global: runs on ALL requests
app.UseGlobal(middleware.Recovery(logger))

// Group: runs on all /api/* routes
api := app.Group("/api")
api.Use(middleware.Logging(logger))

// Route: runs only on this endpoint
api.POST("/users", createUser,
    forge.WithMiddleware(rateLimitMiddleware),
)

Practical Example

A production API with layered middleware:

func setupRoutes(app forge.Router, logger forge.Logger) {
    // Global middleware for all routes
    app.UseGlobal(
        middleware.Recovery(logger),
        middleware.RequestID(),
    )

    // Public routes with CORS
    public := app.Group("/api/v1",
        forge.WithGroupMiddleware(
            middleware.CORS(middleware.CORSConfig{
                AllowOrigins: []string{"https://app.example.com"},
                AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
                AllowHeaders: []string{"Content-Type", "Authorization"},
                AllowCredentials: true,
                MaxAge:          3600,
            }),
            middleware.Logging(logger),
            middleware.CompressDefault(),
        ),
    )

    // Rate-limited write operations
    writeLimiter := middleware.NewRateLimiter(50, 100)
    public.POST("/users", createUser,
        forge.WithMiddleware(middleware.RateLimit(writeLimiter, logger)),
    )
    public.GET("/users", listUsers)

    // Internal routes without CORS or compression
    internal := app.Group("/internal",
        forge.WithGroupMiddleware(middleware.Logging(logger)),
    )
    internal.GET("/health", healthCheck)
    internal.GET("/metrics", metricsHandler)
}

How is this guide?

On this page