# Mux - A Lightweight HTTP Router for Go [![Go Version](https://img.shields.io/badge/Go-1.25%2B-00ADD8?style=flat&logo=go)](https://go.dev/) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) 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 ```bash go get code.patial.tech/go/mux ``` ## Quick Start ```go 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](#basic-routing) - [URL Parameters](#url-parameters) - [Middleware](#middleware) - [Route Groups](#route-groups) - [RESTful Resources](#restful-resources) - [Inline Middleware](#inline-middleware) - [Graceful Shutdown](#graceful-shutdown) - [Route Debugging](#route-debugging) - [Complete Example](#complete-example) ## Basic Routing Mux supports all standard HTTP methods: ```go 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()`: ```go 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: ```go // 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: ```go 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: ```go 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: ```go 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: ```go 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` - **Member routes** (`MemberPOST`, `MemberGET`, `MemberPUT`, `MemberPATCH`, `MemberDELETE`): Operate on `/pattern/{id}/action` - Example: `res.MemberPOST("/publish", handler)` → `POST /posts/{id}/publish` ### Resource Middleware Apply middleware to all resource routes: ```go 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: ```go 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: ```go 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 `SIGINT` and `SIGTERM` signals - 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 ```go 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: ```go // 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 ```go 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: ```go 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: ```go 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](LICENSE) file for details. ## Credits Built with âĪïļ using Go's excellent standard library.