Ankit Patial 26bb9bf5ee feat: improve resource routing API and add comprehensive quality standards
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
2025-11-15 14:05:11 +05:30
2025-08-16 11:19:45 +05:30
2025-09-17 22:09:01 +05:30
2025-09-17 22:09:01 +05:30
2024-09-30 13:31:11 +00:00
2025-08-16 19:25:00 +05:30
2025-08-16 11:19:45 +05:30
2025-08-16 19:25:00 +05:30

Mux - A Lightweight HTTP Router for Go

Go Version 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

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

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)

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
  • 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:

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 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

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.

Description
No description provided
Readme MIT 144 KiB
Languages
Go 88.9%
Makefile 5.9%
Shell 5.2%