Cookbook
This page collects ready-to-use patterns for common production scenarios.
Table of Contents
- REST API with versioning
- JSON API with centralized error handling
- Authentication middleware
- JWT authentication
- Request validation
- Pagination
- Graceful shutdown
- Health and readiness endpoints
- Rate limiting per IP
- CORS for a React SPA
- Serving embedded frontend assets
- Request logging with structured output
- Testing handlers with MuxMaster
- Feature-based file structure
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
- Getting Started — step-by-step introduction
- Middleware — all built-in middleware
- Error Handling — centralized error patterns
- Groups — organizing routes
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.