On this page

Middleware

Middleware in MuxMaster is any function with the signature:

func(http.Handler) http.Handler

This is the same signature used by net/http, chi, gorilla/mux, and most other Go web libraries, so any existing middleware is compatible with MuxMaster without modification.

Table of Contents


How Middleware Works

MuxMaster applies middleware at route registration time. When you call mux.Use(mw) and then mux.GET("/path", handler), the handler stored in the router is mw(handler) — not the original handler plus a middleware list.

This means:

  • Zero per-request overhead from iterating a middleware chain
  • Middleware applied to a route stays with that route, regardless of later Use calls
  • Use must be called before the routes it should affect

Execution order mirrors nesting order: the first middleware listed in Use is the outermost wrapper (runs first on request, last on response).

mux.Use(A)
mux.Use(B)
mux.GET("/path", handler)
// Execution: A → B → handler → B → A

Global Middleware

Use appends middleware to the mux's global chain. It applies to all routes registered after the call:

mux := muxmaster.New()
mux.Use(middleware.Logger(os.Stdout))
mux.Use(middleware.Recoverer)

mux.GET("/api/users", listUsers) // wrapped by Logger and Recoverer

Pre-Routing Middleware

Pre registers middleware that runs before the router matches the request. Use it to rewrite or normalize the URL before the radix tree sees it.

mux.Pre(middleware.CleanPath)
mux.Pre(middleware.StripSlashes)

Pre-routing middleware cannot access path parameters because routing has not happened yet. It is useful for path normalization, request ID injection, and real IP extraction.


Group Middleware

Middleware registered on a group applies only to the routes in that group, after any mux-level middleware:

mux := muxmaster.New()
mux.Use(middleware.Logger(os.Stdout)) // runs for all routes

api := mux.Group("/api/v1")
api.Use(requireAPIKey)  // runs only for routes in /api/v1

api.GET("/users", listUsers) // Logger → requireAPIKey → listUsers
mux.GET("/health", health)     // Logger → health (no requireAPIKey)

Per-Route Middleware with With

With returns a copy of the mux or group with additional middleware scoped to the next route registration:

// On the mux
mux.With(requireAdmin).DELETE("/users/:id", deleteUser)

// On a group
api.With(rateLimit, auditLog).POST("/payments", processPayment)

With does not mutate the original mux or group; it returns a new, temporary scope.


Writing Custom Middleware

A middleware is a function that receives the next handler and returns a new handler:

func requireAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if !isValidToken(token) {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

mux.Use(requireAuth)

Passing configuration to middleware

Wrap the middleware function in a constructor that accepts options:

func RateLimit(requestsPerSecond int) func(http.Handler) http.Handler {
    limiter := rate.NewLimiter(rate.Limit(requestsPerSecond), requestsPerSecond)
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

mux.Use(RateLimit(100))

Sharing data between middleware and handlers via context

Use context.WithValue with an unexported key type to avoid collisions:

type ctxKey string

const userIDKey ctxKey = "userID"

func injectUserID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := extractUserIDFromToken(r.Header.Get("Authorization"))
        ctx := context.WithValue(r.Context(), userIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// In a handler:
userID := r.Context().Value(userIDKey).(string)

Alternatively, use middleware.WithValue for simple cases:

mux.Use(middleware.WithValue("requestEnv", "production"))

// In a handler:
env := r.Context().Value("requestEnv").(string)

Built-in Middleware Reference

Import the middleware sub-package:

import "github.com/FlavioCFOliveira/MuxMaster/middleware"

Logger

Logs each request after it completes. Output format: timestamp method path status duration.

mux.Use(middleware.Logger(os.Stdout))

Sample output:

2026-04-17T10:05:31Z GET /api/v1/users 200 1.243ms
2026-04-17T10:05:32Z POST /api/v1/users 201 4.871ms

Parameters:

  • out io.Writer — destination for log lines; panics if nil

Recoverer

Catches panics in downstream handlers, writes a 500 response, and resumes normal request processing. Without this middleware a panic crashes the entire server.

mux.Use(middleware.Recoverer)

CORS

Handles Cross-Origin Resource Sharing. Responds to preflight OPTIONS requests and sets the appropriate CORS headers on responses.

mux.Use(middleware.CORS(middleware.CORSOptions{
    AllowedOrigins:   []string{"https://app.example.com"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Authorization", "Content-Type"},
    AllowCredentials: true,
    MaxAge:           86400, // seconds to cache preflight response
}))

To allow all origins (not recommended for authenticated APIs):

mux.Use(middleware.CORS(middleware.CORSOptions{
    AllowedOrigins: []string{"*"},
    AllowedMethods: []string{"GET", "POST"},
}))

CORSOptions fields:

Field Type Description
AllowedOrigins []string Origins that may access the resource
AllowedMethods []string HTTP methods allowed in the actual request
AllowedHeaders []string Request headers that may be used
ExposedHeaders []string Response headers accessible to the browser
AllowCredentials bool Whether the response can include cookies (cannot use "*")
MaxAge int Seconds to cache the preflight response

BasicAuth

Requires HTTP Basic Authentication credentials for the wrapped route or group.

credentials := map[string]string{
    "admin":  "secret",
    "reader": "readonly",
}
mux.Use(middleware.BasicAuth("My API", credentials))

Parameters:

  • realm string — shown to the user in the browser's credential prompt
  • credentials map[string]string — map of username → password

Compress

Compresses responses using gzip or deflate, depending on the Accept-Encoding header.

mux.Use(middleware.Compress(5)) // compression level 1–9; 5 is a good default

Responses smaller than a threshold are not compressed. The Content-Encoding: gzip header is set automatically.


ThrottleBacklog

Limits the number of concurrently executing handlers. Requests that exceed the limit are queued; requests that exceed the queue are rejected with 503.

mux.Use(middleware.ThrottleBacklog(
    100,                // max concurrent handlers
    50,                 // max queued requests
    30*time.Second,     // max time a request may wait in the queue
))

Parameters:

  • limit int — maximum number of handlers running simultaneously; must be > 0
  • backlog int — maximum number of requests waiting for a slot; must be ≥ 0
  • timeout time.Duration — how long a queued request waits before receiving a 503

Timeout

Cancels the request context after the specified duration. The handler is expected to honour ctx.Done() to exit early.

mux.Use(middleware.Timeout(10 * time.Second))

The timeout applies to the handler execution time, not to the total connection lifetime.


RequestID

Attaches a unique request ID to every request. Reads X-Request-Id from the incoming headers; generates a random UUID if absent. Writes the ID back in the response as X-Request-Id.

mux.Use(middleware.RequestID)

To read the request ID in a handler:

id := r.Header.Get("X-Request-Id")

RealIP

Extracts the real client IP address from X-Forwarded-For or X-Real-IP headers set by a reverse proxy, and sets r.RemoteAddr to that value.

mux.Use(middleware.RealIP)

Only use this middleware if the server is behind a trusted reverse proxy. Accepting these headers from arbitrary clients is a security risk.


CleanPath

Redirects URLs with redundant components to their canonical form:

  • //users/users
  • /a/../users/users
  • /a/./users/a/users
mux.Pre(middleware.CleanPath)  // run before routing to avoid a redirect

StripSlashes

Removes trailing slashes from the URL path before routing. Unlike RedirectTrailingSlash, this modifies the request in-place without issuing a redirect.

mux.Pre(middleware.StripSlashes)

NoCache

Sets headers that instruct browsers and intermediaries not to cache the response.

mux.Use(middleware.NoCache)

Headers set: Cache-Control: no-cache, no-store, no-transform, must-revalidate, private, max-age=0, Pragma: no-cache, Expires: 0.


SetHeader

Sets a fixed response header for every request:

mux.Use(middleware.SetHeader("X-Content-Type-Options", "nosniff"))
mux.Use(middleware.SetHeader("X-Frame-Options", "DENY"))
mux.Use(middleware.SetHeader("Strict-Transport-Security", "max-age=31536000"))

WithValue

Stores a value in the request context. Useful for injecting configuration or feature flags:

mux.Use(middleware.WithValue("appEnv", "production"))

// In a handler:
env := r.Context().Value("appEnv").(string)

See Also

  • Groups — applying middleware to a subset of routes
  • Error Handling — error-returning handlers
  • Cookbook — middleware composition patterns

Upstream source

The middleware chain composition and the Use / Pre ordering described above are implemented in handler.go in the upstream repository.

Common questions

How do I add a middleware that runs on every request?

Call m.Use(middleware) on the top-level mux before registering any routes. The middleware wraps every handler the router dispatches, so request-id, recovery, and structured logging are the typical candidates for global Use.

How do I order multiple middlewares?

The order of Use calls is the order of execution: the first call wraps the outermost layer (the request enters it first and leaves it last). Reverse the registration order to reverse the chain — there is no priority field.

When should I prefer Pre over Use?

Pick Pre for middleware that must run before the router resolves the route — typically RealIP, CleanPath, or a TLS-redirect step. Pre runs against the raw URL before pattern matching; Use runs after the route is selected and the handler chain is composed.