Files
mux/README.md
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

582 lines
15 KiB
Markdown

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