Routegroup
Lightweight library for route grouping and middleware integration with the standard http.ServeMux
Install / Use
/learn @go-pkgz/RoutegroupREADME
routegroup

routegroup is a tiny Go package providing a lightweight wrapper for efficient route grouping and middleware integration with the standard http.ServeMux.
Features
- Simple and intuitive API for route grouping and route mounting.
- Lightweight, just about 100 LOC
- Easy middleware integration for individual routes or groups of routes.
- Seamless integration with Go's standard
http.ServeMux. - Fully compatible with the
http.Handlerinterface and can be used as a drop-in replacement forhttp.ServeMux. - No external dependencies.
Requirements
- Go 1.23 or higher
(This library uses
http.Request.Patternto make route patterns available to global middlewares and relies on the enhancedhttp.ServeMuxrouting behavior introduced in Go 1.22/1.23)
Install and update
go get -u github.com/go-pkgz/routegroup
Usage
Creating a New Route Group
To start, create a new route group without a base path:
func main() {
mux := http.NewServeMux()
group := routegroup.New(mux)
}
Adding Routes with Middleware
Add routes to your group, optionally with middleware:
group.Use(loggingMiddleware, corsMiddleware)
group.Handle("/hello", helloHandler)
group.Handle("/bye", byeHandler)
Creating a Nested Route Group
For routes under a specific path prefix Mount method can be used to create a nested group:
apiGroup := routegroup.Mount(mux, "/api")
apiGroup.Use(loggingMiddleware, corsMiddleware)
apiGroup.Handle("/v1", apiV1Handler)
apiGroup.Handle("/v2", apiV2Handler)
Complete Example
Here's a complete example demonstrating route grouping and middleware usage:
package main
import (
"net/http"
"github.com/go-pkgz/routegroup"
)
func main() {
router := routegroup.New(http.NewServeMux())
router.Use(loggingMiddleware)
// handle the /hello route
router.Handle("GET /hello", helloHandler)
// create a new group for the /api path
apiRouter := router.Mount("/api")
// add middleware
apiRouter.Use(loggingMiddleware, corsMiddleware)
// route handling
apiRouter.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, API!"))
})
// add another group with its own set of middlewares
protectedGroup := router.Group()
protectedGroup.Use(authMiddleware)
protectedGroup.HandleFunc("GET /protected", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Protected API!"))
})
http.ListenAndServe(":8080", router)
}
Applying Middleware to Specific Routes
You can also apply middleware to specific routes inside the group without modifying the group's middleware stack:
apiGroup.With(corsMiddleware, apiMiddleware).Handle("GET /hello", helloHandler)
Alternative Usage with Route
You can also use the Route method to add routes and middleware in a single function call:
router := routegroup.New(http.NewServeMux())
router.Route(func(b *routegroup.Bundle) {
b.Use(loggingMiddleware, corsMiddleware)
b.Handle("GET /hello", helloHandler)
b.Handle("GET /bye", byeHandler)
})
http.ListenAndServe(":8080", router)
When called on the root bundle, Route automatically creates a new group to avoid accidentally modifying the root bundle's middleware stack. This means the middleware and routes defined inside the Route function are isolated from other routes on the root bundle.
The Route method can also be chained after Mount or Group for a more functional style:
router := routegroup.New(http.NewServeMux())
router.Group().Route(func(b *routegroup.Bundle) {
b.Use(loggingMiddleware, corsMiddleware)
b.Handle("GET /hello", helloHandler)
b.Handle("GET /bye", byeHandler)
})
Setting optional NotFoundHandler
It is possible to set a custom NotFoundHandler for the group. This handler will be called when no other route matches the request:
group.NotFoundHandler(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "404 page not found, something is wrong!", http.StatusNotFound)
}
If a custom NotFoundHandler is not configured, routegroup will default to using the standard library behavior.
Note on 405: In the current design, routegroup applies root-level middlewares to all requests at the top level without installing a catch‑all route. This preserves native 405 Method Not Allowed responses from http.ServeMux when a path exists but a wrong method is used. A configured NotFoundHandler is only invoked when no route matches; it does not interfere with 405 handling. The custom NotFoundHandler will have the root bundle's global middlewares applied to it.
Legacy note: DisableNotFoundHandler() is now a no‑op and preserved only for API compatibility.
Middleware Ordering
- Call
Use(...)before registering routes on the same bundle. CallingUseafter any handler has been registered on that bundle will panic with a descriptive error. - Root bundle middlewares (added via
router.Use(...)) are applied globally to all requests at serve time. - Group/bundle middlewares (added via
group.Use(...)) apply to the routes registered on that bundle and its descendants, provided they are added before those routes. With(...)returns a new bundle; you can add middlewares there first, then register routes. This is the preferred way to add scoped middlewares without affecting previously defined routes.
Important: Route registration (HandleFunc, Handle, HandleFiles, etc.) should be done during initialization and not performed concurrently. The library is designed for typical usage where routes are registered at startup time in a single goroutine.
Examples
Incorrect: calling Use after routes on the same bundle (will panic)
mux := http.NewServeMux()
router := routegroup.New(mux)
router.HandleFunc("/r", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })
// This will panic: Use called after routes were registered on this bundle
router.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// global header
w.Header().Set("X-Global", "true")
next.ServeHTTP(w, r)
})
})
Allowed: parent/root Use after child bundle routes
mux := http.NewServeMux()
router := routegroup.New(mux)
child := router.Group()
child.HandleFunc("/child", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("ok")) })
// Parent has not registered its own routes yet; this is allowed and will apply globally
router.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Parent", "true")
next.ServeHTTP(w, r)
})
})
Preferred: use With (or Group+Use) to attach scoped middleware before routes
mux := http.NewServeMux()
router := routegroup.New(mux)
// Global middleware (optional), add before any root routes
router.Use(loggingMiddleware)
// Scoped middleware using With: returns a new bundle on which we can add routes
api := router.With(authMiddleware)
api.HandleFunc("GET /items", itemsHandler)
api.HandleFunc("POST /items", createItem)
// Or using Group + Use before routes
admin := router.Group()
admin.Use(adminOnly)
admin.HandleFunc("GET /dashboard", dashboardHandler)
Handling Root Paths Without Trailing Slashes
When working with mounted groups, you often need to handle requests to the group's root path without a trailing slash. For this purpose, routegroup provides the HandleRoot or HandleRootFunc methods:
// Create mounted groups
apiGroup := router.Mount("/api")
v1Group := apiGroup.Mount("/v1")
usersGroup := v1Group.Mount("/users")
// Handle the root paths (no trailing slashes)
apiGroup.HandleRoot("GET", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// This handles requests to "/api" (without trailing slash)
w.Write([]byte("API Documentation"))
}))
usersGroup.HandleRoot("GET", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// This handles requests to "/api/v1/users" (without trailing slash)
w.Write([]byte("List users"))
}))
// Different HTTP methods can be handled separately
usersGroup.HandleRoot("POST", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// This handles POST requests to "/api/v1/users"
w.Write([]byte("Create user"))
}))
While it's also possible to handle such paths using a trailing slash pattern ("/") with the regular Handle or HandleFunc methods, that approach results in a redirect from non-trailing slash URLs (e.g., /api) to the trailing slash version (e.g., /api/). The HandleRoot method avoids this redirect, providing a more direct response and avoiding an extra round-trip, which is especially important for non-GET requests or when clients don't automatically follow redirects.
Using derived groups
In some instances, it's practical to create an initial group that includes a set of middlewares, and then derive all other groups from it. This approach guarantees that every group incorporates a common set of middlewares as a foundation, allowing each to add its specific middlewares. To facilitate this scenario, routegroup offers both Bundle.Group and Bundle.Mount methods, and it also implements the http.Handler interface. The following example ill
Related Skills
node-connect
346.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
107.2kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
346.4kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
346.4kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
