BREAKING CHANGES:
- Renamed HandleGET -> MemberGET for member-level routes
- Renamed HandlePOST -> MemberPOST for member-level routes
- Renamed HandlePUT -> MemberPUT for member-level routes
- Renamed HandlePATCH -> MemberPATCH for member-level routes
- Renamed HandleDELETE -> MemberDELETE for member-level routes
New Features:
- Added collection-level route methods: GET, POST, PUT, PATCH, DELETE
- Clear distinction between collection (/pattern/action) and member (/pattern/{id}/action) routes
- Comprehensive documentation (README, CONTRIBUTING, QUICKSTART, DOCS)
- Development tooling (Makefile, check.sh script)
- AI coding assistant guidelines (.cursorrules)
- GitHub Actions CI/CD pipeline
- golangci-lint configuration
Code Quality:
- Optimized struct field alignment for better memory usage
- All code passes go vet, staticcheck, and fieldalignment
- All tests pass with race detector
- Go 1.25+ requirement enforced
Documentation:
- Complete README rewrite with examples
- CONTRIBUTING.md with development guidelines
- QUICKSTART.md for new users
- CHANGELOG.md with version history
- SUMMARY.md documenting all changes
- DOCS.md as documentation index
Mux - A Lightweight HTTP Router for Go
Mux is a simple, lightweight HTTP router for Go that wraps around the standard http.ServeMux to provide additional functionality and a more ergonomic API for building web applications and APIs.
Features
- 🚀 Built on top of Go's standard
http.ServeMux(Go 1.22+ routing enhancements) - 🎯 HTTP method-specific routing (GET, POST, PUT, DELETE, PATCH, etc.)
- 🔌 Flexible middleware support with stackable composition
- 📦 Route grouping for organization and shared middleware
- 🎨 RESTful resource routing with collection and member routes
- 🔗 URL parameter extraction using Go's standard path values
- 🛡️ Graceful shutdown support with signal handling
- 📋 Route listing and debugging
- ⚡ Zero external dependencies (only Go standard library)
- 🪶 Minimal overhead and excellent performance
Requirements
- Go 1.25 or higher
Installation
go get code.patial.tech/go/mux
Quick Start
package main
import (
"fmt"
"net/http"
"code.patial.tech/go/mux"
)
func main() {
// Create a new router
m := mux.New()
// Define routes
m.GET("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, World!")
})
m.GET("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "User ID: %s", id)
})
// Start server with graceful shutdown
m.Serve(func(srv *http.Server) error {
srv.Addr = ":8080"
return srv.ListenAndServe()
})
}
Table of Contents
- Basic Routing
- URL Parameters
- Middleware
- Route Groups
- RESTful Resources
- Inline Middleware
- Graceful Shutdown
- Route Debugging
- Complete Example
Basic Routing
Mux supports all standard HTTP methods:
m := mux.New()
// HTTP method routes
m.GET("/users", listUsers)
m.POST("/users", createUser)
m.PUT("/users/{id}", updateUser)
m.PATCH("/users/{id}", partialUpdateUser)
m.DELETE("/users/{id}", deleteUser)
m.HEAD("/users", headUsers)
m.OPTIONS("/users", optionsUsers)
m.CONNECT("/proxy", connectProxy)
m.TRACE("/debug", traceDebug)
URL Parameters
Extract URL parameters using Go's standard r.PathValue():
m.GET("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id")
fmt.Fprintf(w, "Fetching user: %s", userID)
})
m.GET("/posts/{year}/{month}/{slug}", func(w http.ResponseWriter, r *http.Request) {
year := r.PathValue("year")
month := r.PathValue("month")
slug := r.PathValue("slug")
// ... handle request
})
Middleware
Middleware functions follow the standard func(http.Handler) http.Handler signature, making them compatible with most Go middleware libraries.
Global Middleware
Apply middleware to all routes:
// Logging middleware
func logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.Info("request", "method", r.Method, "path", r.URL.Path)
next.ServeHTTP(w, r)
})
}
// Authentication middleware
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
m := mux.New()
m.Use(logger)
m.Use(auth)
// All routes will use logger and auth middleware
m.GET("/protected", protectedHandler)
Compatible with Popular Middleware
Works with any middleware following the standard signature:
import (
"github.com/gorilla/handlers"
chimiddleware "github.com/go-chi/chi/v5/middleware"
)
m.Use(handlers.CompressHandler)
m.Use(chimiddleware.RealIP)
m.Use(chimiddleware.Recoverer)
Route Groups
Organize routes and apply middleware to specific groups:
m := mux.New()
// Public routes
m.GET("/", homeHandler)
m.GET("/about", aboutHandler)
// API routes with shared middleware
m.Group(func(api *mux.Mux) {
api.Use(jsonMiddleware)
api.Use(authMiddleware)
api.GET("/api/users", listUsersAPI)
api.POST("/api/users", createUserAPI)
api.DELETE("/api/users/{id}", deleteUserAPI)
})
// Admin routes with different middleware
m.Group(func(admin *mux.Mux) {
admin.Use(adminAuthMiddleware)
admin.Use(auditLogMiddleware)
admin.GET("/admin/dashboard", dashboardHandler)
admin.GET("/admin/users", adminUsersHandler)
})
RESTful Resources
Define RESTful resources with conventional routing:
m.Resource("/posts", func(res *mux.Resource) {
// Standard RESTful routes
res.Index(listPosts) // GET /posts
res.CreateView(newPostForm) // GET /posts/create
res.Create(createPost) // POST /posts
res.View(showPost) // GET /posts/{id}
res.Update(updatePost) // PUT /posts/{id}
res.UpdatePartial(patchPost) // PATCH /posts/{id}
res.Delete(deletePost) // DELETE /posts/{id}
})
Custom Resource Routes
Add custom routes at collection or member level:
m.Resource("/posts", func(res *mux.Resource) {
// Standard routes
res.Index(listPosts)
res.View(showPost)
// Collection-level custom routes (on /posts/...)
res.POST("/search", searchPosts) // POST /posts/search
res.GET("/archived", archivedPosts) // GET /posts/archived
res.GET("/trending", trendingPosts) // GET /posts/trending
// Member-level custom routes (on /posts/{id}/...)
res.MemberPOST("/publish", publishPost) // POST /posts/{id}/publish
res.MemberPOST("/archive", archivePost) // POST /posts/{id}/archive
res.MemberGET("/comments", getComments) // GET /posts/{id}/comments
res.MemberDELETE("/cache", clearCache) // DELETE /posts/{id}/cache
})
Collection vs Member Routes
-
Collection routes (
POST,GET,PUT,PATCH,DELETE): Operate on/pattern/action- Example:
res.POST("/search", handler)→POST /posts/search
- Example:
-
Member routes (
MemberPOST,MemberGET,MemberPUT,MemberPATCH,MemberDELETE): Operate on/pattern/{id}/action- Example:
res.MemberPOST("/publish", handler)→POST /posts/{id}/publish
- Example:
Resource Middleware
Apply middleware to all resource routes:
m.Resource("/posts", func(res *mux.Resource) {
// Middleware for all routes in this resource
res.Use(postAuthMiddleware)
res.Use(postValidationMiddleware)
res.Index(listPosts)
res.Create(createPost)
// ... other routes
}, resourceSpecificMiddleware) // Can also pass middleware as arguments
Inline Middleware
Apply middleware to specific routes without affecting others:
m := mux.New()
// Route without middleware
m.GET("/public", publicHandler)
// Route with inline middleware
m.With(authMiddleware, rateLimitMiddleware).
GET("/protected", protectedHandler)
// Another route with different middleware
m.With(adminMiddleware).
POST("/admin/action", adminActionHandler)
Graceful Shutdown
Built-in graceful shutdown with signal handling:
m := mux.New()
// Define routes...
m.GET("/", homeHandler)
// Serve with graceful shutdown
m.Serve(func(srv *http.Server) error {
srv.Addr = ":8080"
srv.ReadTimeout = 10 * time.Second
srv.WriteTimeout = 10 * time.Second
slog.Info("Server starting", "addr", srv.Addr)
return srv.ListenAndServe()
})
Features:
- Listens for
SIGINTandSIGTERMsignals - Drains existing connections (10 second grace period)
- Propagates shutdown signal to handlers via
m.IsShuttingDown - Automatic OPTIONS handler for all routes
- Hard shutdown after grace period to prevent hanging
Checking Shutdown Status
m.GET("/health", func(w http.ResponseWriter, r *http.Request) {
if m.IsShuttingDown.Load() {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("Server is shutting down"))
return
}
w.Write([]byte("OK"))
})
Route Debugging
List all registered routes:
// Print routes to stdout
m.PrintRoutes(os.Stdout)
// Get routes as slice
routes := m.RouteList()
for _, route := range routes {
fmt.Println(route)
}
// Expose routes via HTTP endpoint
m.GET("/debug/routes", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
m.PrintRoutes(w)
})
Output example:
GET /
GET /users
POST /users
GET /users/{id}
PUT /users/{id}
DELETE /users/{id}
GET /posts
POST /posts/search
GET /posts/{id}
POST /posts/{id}/publish
Complete Example
package main
import (
"fmt"
"log/slog"
"net/http"
"os"
"time"
"code.patial.tech/go/mux"
)
func main() {
m := mux.New()
// Global middleware
m.Use(loggingMiddleware)
m.Use(recoveryMiddleware)
// Public routes
m.GET("/", homeHandler)
m.GET("/about", aboutHandler)
// API group
m.Group(func(api *mux.Mux) {
api.Use(jsonMiddleware)
// Users resource
api.Resource("/users", func(res *mux.Resource) {
res.Index(listUsers)
res.Create(createUser)
res.View(showUser)
res.Update(updateUser)
res.Delete(deleteUser)
// Custom user actions
res.POST("/search", searchUsers)
res.MemberPOST("/activate", activateUser)
res.MemberGET("/posts", getUserPosts)
})
// Posts resource
api.Resource("/posts", func(res *mux.Resource) {
res.Index(listPosts)
res.Create(createPost)
res.View(showPost)
res.MemberPOST("/publish", publishPost)
res.MemberGET("/comments", getPostComments)
})
})
// Debug route
m.GET("/debug/routes", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
m.PrintRoutes(w)
})
// Start server
m.Serve(func(srv *http.Server) error {
srv.Addr = ":8080"
srv.ReadTimeout = 30 * time.Second
srv.WriteTimeout = 30 * time.Second
slog.Info("Server listening", "addr", srv.Addr)
return srv.ListenAndServe()
})
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"duration", time.Since(start))
})
}
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
slog.Error("panic recovered", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func jsonMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}
// Handler implementations
func homeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Welcome Home!")
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "About Us")
}
func listUsers(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"users": []}`)
}
func createUser(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"message": "User created"}`)
}
func showUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, `{"id": "%s"}`, id)
}
func updateUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, `{"message": "User %s updated"}`, id)
}
func deleteUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, `{"message": "User %s deleted"}`, id)
}
func searchUsers(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"results": []}`)
}
func activateUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, `{"message": "User %s activated"}`, id)
}
func getUserPosts(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, `{"user_id": "%s", "posts": []}`, id)
}
func listPosts(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"posts": []}`)
}
func createPost(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"message": "Post created"}`)
}
func showPost(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, `{"id": "%s"}`, id)
}
func publishPost(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, `{"message": "Post %s published"}`, id)
}
func getPostComments(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, `{"post_id": "%s", "comments": []}`, id)
}
Custom 404 Handler
Handle 404 errors with a catch-all route:
m.GET("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.Error(w, "404 - Page Not Found", http.StatusNotFound)
return
}
// Handle root path
fmt.Fprint(w, "Home Page")
})
Testing
Testing routes is straightforward using Go's httptest package:
func TestHomeHandler(t *testing.T) {
m := mux.New()
m.GET("/", homeHandler)
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
m.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
}
Performance
Mux has minimal overhead since it wraps Go's standard http.ServeMux:
- Zero heap allocations for simple routes
- Efficient middleware chaining using composition
- Fast pattern matching powered by Go's stdlib
- No reflection or runtime code generation
Contributing
Contributions are welcome! Please feel free to submit issues, fork the repository, and create pull requests.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Credits
Built with ❤️ using Go's excellent standard library.