On this page

Cookbook

This page collects ready-to-use patterns for common production scenarios.

Table of Contents


REST API with versioning

func main() {
    mux := muxmaster.New()
    mux.Use(middleware.Logger(os.Stdout))
    mux.Use(middleware.Recoverer)
    mux.Use(middleware.RequestID)

    mux.Mount("/api/v1", v1Router())
    mux.Mount("/api/v2", v2Router())

    mux.GET("/health", func(w http.ResponseWriter, r *http.Request) {
        muxmaster.JSON(w, http.StatusOK, map[string]string{"status": "ok"})
    })

    log.Fatal(http.ListenAndServe(":8080", mux))
}

func v1Router() http.Handler {
    mux := muxmaster.New()
    mux.Use(requireAPIKey)

    mux.GET("/users", listUsersV1)
    mux.POST("/users", createUserV1)
    mux.GET("/users/:id", getUserV1)

    return mux
}

func v2Router() http.Handler {
    mux := muxmaster.New()
    mux.Use(requireAPIKey)

    mux.GET("/users", listUsersV2) // new response shape
    mux.POST("/users", createUserV2)
    mux.GET("/users/:id", getUserV2)
    mux.GET("/users/:id/posts", getUserPostsV2) // new in v2

    return mux
}

JSON API with centralized error handling

type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

func setupRouter() *muxmaster.Mux {
    mux := muxmaster.New()

    mux.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
        code := http.StatusInternalServerError
        msg  := "internal server error"

        var he muxmaster.HTTPError
        if errors.As(err, &he) {
            code = he.StatusCode()
            msg  = err.Error()
        } else {
            slog.Error("unhandled error",
                "method", r.Method,
                "path",   r.URL.Path,
                "error",  err,
            )
        }

        muxmaster.JSON(w, code, APIError{Code: code, Message: msg})
    }

    mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        muxmaster.JSON(w, http.StatusNotFound, APIError{
            Code: http.StatusNotFound, Message: "not found",
        })
    })

    mux.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        muxmaster.JSON(w, http.StatusMethodNotAllowed, APIError{
            Code: http.StatusMethodNotAllowed, Message: "method not allowed",
        })
    })

    return mux
}

Authentication middleware

type ctxKey string

const claimsKey ctxKey = "claims"

type Claims struct {
    UserID int
    Role   string
}

func requireAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
        claims, err := verifyToken(token)
        if err != nil {
            muxmaster.JSON(w, http.StatusUnauthorized, map[string]string{
                "error": "invalid or missing token",
            })
            return
        }
        ctx := context.WithValue(r.Context(), claimsKey, claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func requireRole(role string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            claims := r.Context().Value(claimsKey).(*Claims)
            if claims.Role != role {
                muxmaster.JSON(w, http.StatusForbidden, map[string]string{
                    "error": "insufficient permissions",
                })
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// Usage
api := mux.Group("/api/v1")
api.Use(requireAuth)

api.GET("/profile", getProfile)                              // any authenticated user
api.With(requireRole("admin")).DELETE("/users/:id", deleteUser) // admin only

JWT authentication

import "github.com/golang-jwt/jwt/v5"

var jwtSecret = []byte(os.Getenv("JWT_SECRET"))

type JWTClaims struct {
    UserID int    `json:"uid"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func jwtMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        raw := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
        token, err := jwt.ParseWithClaims(raw, &JWTClaims{},
            func(t *jwt.Token) (any, error) {
                if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
                    return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
                }
                return jwtSecret, nil
            },
        )
        if err != nil || !token.Valid {
            muxmaster.JSON(w, http.StatusUnauthorized, map[string]string{
                "error": "invalid token",
            })
            return
        }
        claims := token.Claims.(*JWTClaims)
        ctx := context.WithValue(r.Context(), claimsKey, claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Request validation

A simple approach using a validate helper that returns an HTTPError:

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func (req *CreateUserRequest) Validate() error {
    if req.Name == "" {
        return muxmaster.Error(http.StatusUnprocessableEntity,
            errors.New("name is required"))
    }
    if !strings.Contains(req.Email, "@") {
        return muxmaster.Error(http.StatusUnprocessableEntity,
            errors.New("email is invalid"))
    }
    return nil
}

mux.POSTE("/users", func(w http.ResponseWriter, r *http.Request) error {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        return muxmaster.Error(http.StatusBadRequest, err)
    }
    if err := req.Validate(); err != nil {
        return err
    }
    user, err := db.CreateUser(req.Name, req.Email)
    if err != nil {
        return err
    }
    return muxmaster.JSON(w, http.StatusCreated, user)
})

Pagination

type PageRequest struct {
    Page  int
    Limit int
}

func parsePage(r *http.Request) PageRequest {
    page, _ := strconv.Atoi(r.URL.Query().Get("page"))
    limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
    if page < 1 {
        page = 1
    }
    if limit < 1 || limit > 100 {
        limit = 20
    }
    return PageRequest{Page: page, Limit: limit}
}

mux.GET("/users", func(w http.ResponseWriter, r *http.Request) {
    pg := parsePage(r)
    users, total, err := db.ListUsers(pg.Page, pg.Limit)
    if err != nil {
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
    muxmaster.JSON(w, http.StatusOK, map[string]any{
        "data":  users,
        "total": total,
        "page":  pg.Page,
        "limit": pg.Limit,
    })
})

Graceful shutdown

func main() {
    mux := muxmaster.New()
    // ... register routes ...

    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    // Start server in a goroutine
    go func() {
        log.Println("listening on :8080")
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %v", err)
        }
    }()

    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("shutting down...")

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("shutdown: %v", err)
    }
    log.Println("server stopped")
}

Health and readiness endpoints

mux.GET("/health", func(w http.ResponseWriter, r *http.Request) {
    muxmaster.JSON(w, http.StatusOK, map[string]string{"status": "ok"})
})

mux.GET("/ready", func(w http.ResponseWriter, r *http.Request) {
    if err := db.Ping(); err != nil {
        muxmaster.JSON(w, http.StatusServiceUnavailable, map[string]string{
            "status": "unavailable",
            "reason": "database unreachable",
        })
        return
    }
    muxmaster.JSON(w, http.StatusOK, map[string]string{"status": "ready"})
})

Rate limiting per IP

import "golang.org/x/time/rate"

func ipRateLimiter(rps float64, burst int) func(http.Handler) http.Handler {
    mu       := sync.Mutex{}
    limiters := map[string]*rate.Limiter{}

    getLimiter := func(ip string) *rate.Limiter {
        mu.Lock()
        defer mu.Unlock()
        if l, ok := limiters[ip]; ok {
            return l
        }
        l := rate.NewLimiter(rate.Limit(rps), burst)
        limiters[ip] = l
        return l
    }

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ip := r.RemoteAddr // use middleware.RealIP if behind a proxy
            if !getLimiter(ip).Allow() {
                muxmaster.JSON(w, http.StatusTooManyRequests, map[string]string{
                    "error": "too many requests",
                })
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

mux.Use(ipRateLimiter(10, 30)) // 10 req/s, burst of 30

CORS for a React SPA

mux.Use(middleware.CORS(middleware.CORSOptions{
    AllowedOrigins: []string{
        "http://localhost:3000",         // local development
        "https://myapp.example.com",     // production
    },
    AllowedMethods:   []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Authorization", "Content-Type", "X-Request-Id"},
    ExposedHeaders:   []string{"X-Request-Id"},
    AllowCredentials: true,
    MaxAge:           86400,
}))

Put CORS middleware before authentication middleware so preflight OPTIONS requests (which do not carry credentials) are handled without authentication.


Serving embedded frontend assets

import "embed"

//go:embed frontend/dist
var frontendFS embed.FS

func main() {
    mux := muxmaster.New()

    // API routes
    api := mux.Group("/api")
    api.GET("/users", listUsers)

    // Frontend — serve index.html for all unmatched routes (SPA fallback)
    mux.GET("/*filepath", func(w http.ResponseWriter, r *http.Request) {
        path := muxmaster.PathParam(r, "filepath")
        f, err := frontendFS.Open("frontend/dist" + path)
        if err != nil {
            // File not found — serve index.html for client-side routing
            http.ServeFileFS(w, r, frontendFS, "frontend/dist/index.html")
            return
        }
        f.Close()
        http.ServeFileFS(w, r, frontendFS, "frontend/dist"+path)
    })

    log.Fatal(http.ListenAndServe(":8080", mux))
}

Request logging with structured output

func structuredLogger(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rec   := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
        next.ServeHTTP(rec, r)
        slog.Info("request",
            "method",    r.Method,
            "path",      r.URL.Path,
            "status",    rec.status,
            "duration",  time.Since(start),
            "ip",        r.RemoteAddr,
            "requestID", r.Header.Get("X-Request-Id"),
        )
    })
}

type statusRecorder struct {
    http.ResponseWriter
    status int
}
func (r *statusRecorder) WriteHeader(code int) {
    r.status = code
    r.ResponseWriter.WriteHeader(code)
}

mux.Use(structuredLogger)

Testing handlers with MuxMaster

Test handlers by creating a router, registering the routes under test, and using httptest:

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestGetUser(t *testing.T) {
    mux := muxmaster.New()
    mux.GET("/users/:id", getUser)

    req  := httptest.NewRequest("GET", "/users/42", nil)
    rec  := httptest.NewRecorder()
    mux.ServeHTTP(rec, req)

    if rec.Code != http.StatusOK {
        t.Fatalf("expected 200, got %d", rec.Code)
    }
}

Testing with a custom error handler:

func TestCreateUserValidation(t *testing.T) {
    mux := muxmaster.New()
    mux.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
        var he muxmaster.HTTPError
        if errors.As(err, &he) {
            w.WriteHeader(he.StatusCode())
            return
        }
        w.WriteHeader(http.StatusInternalServerError)
    }
    mux.POSTE("/users", createUser)

    body := strings.NewReader(`{}`) // missing required fields
    req  := httptest.NewRequest("POST", "/users", body)
    req.Header.Set("Content-Type", "application/json")
    rec  := httptest.NewRecorder()
    mux.ServeHTTP(rec, req)

    if rec.Code != http.StatusUnprocessableEntity {
        t.Fatalf("expected 422, got %d", rec.Code)
    }
}

Feature-based file structure

For larger applications, organize code by domain rather than by layer:

myapi/
├── main.go
├── internal/
│   ├── user/
│   │   ├── handler.go    // HTTP handlers
│   │   ├── service.go    // business logic
│   │   ├── repository.go // database access
│   │   └── routes.go     // route registration
│   ├── order/
│   │   ├── handler.go
│   │   ├── service.go
│   │   ├── repository.go
│   │   └── routes.go
│   └── middleware/
│       └── auth.go
└── go.mod

Each routes.go registers routes on a group passed from main.go:

// internal/user/routes.go
func RegisterRoutes(g *muxmaster.Group, svc *Service) {
    h := &Handler{svc: svc}
    g.GET("/users",      h.List)
    g.POST("/users",     h.Create)
    g.GET("/users/:id",  h.Get)
    g.PUT("/users/:id",  h.Update)
    g.DELETE("/users/:id", h.Delete)
}

// main.go
api := mux.Group("/api/v1")
api.Use(middleware.RequireAuth)

user.RegisterRoutes(api, userService)
order.RegisterRoutes(api, orderService)

See Also

Upstream source

Every recipe in this cookbook traces back to a runnable program in the upstream examples/ directory.

Common questions

How do I structure a production-ready application with MuxMaster?

Split the registration into a function (func registerRoutes(m *mux.Mux, deps Deps)) so tests can construct the same router without starting a server. Group middleware by concern (auth, observability, body-limits) and apply them with Use at the appropriate scope (root for global, group for scoped).

How do I share state across handlers?

Inject the dependency at construction time — for example func newAPIHandler(db *sql.DB) http.HandlerFunc returns a closure that captures the dependency. The router never owns the dependency; this keeps tests free of global state and avoids context-key gymnastics.

How do I write a unit test that exercises my routes?

Construct the router in the test (the same registerRoutes function the binary calls), use httptest.NewRecorder to capture the response, and call m.ServeHTTP(rr, req). No external HTTP layer is needed; the router implements http.Handler, so the same test pattern works with any test fixture style.