Middleware
Middleware has the shape most Go middleware already has, so existing middleware works as it is:
type Middleware = func(http.Handler) http.HandlerOn fasthttp it is func(fasthttp.RequestHandler) fasthttp.RequestHandler.
Three places to put it
Section titled “Three places to put it”| Where | Call | Wraps |
|---|---|---|
| Around the whole router | gorouter.New(gorouter.WithMiddleware(mw...)) |
Every request, including 404, 405 and automatic OPTIONS responses |
| Below a path | r.Use(pattern, mw...) |
Every route at or below pattern, on every method |
| One method below a path | r.UseMethod(method, pattern, mw...) |
Routes at or below pattern on method |
r := gorouter.New(gorouter.WithMiddleware(logRequests, recoverPanics))r.Use("/api", noStore)r.UseMethod(http.MethodPost, "/api", requireJSON)Use matches whole segments: r.Use("/api", mw) covers /api and /api/users, never /apiary.
Params work too: r.Use("/users/{id}", mw) covers /users/{id} and everything below it. Only a param’s position counts, not its name, so it also covers /users/{name}/posts.
Pass "/" to cover every route.
Middleware runs in the order it was registered: the first one is the outermost. Router-wide middleware runs first, then Use and UseMethod middleware in registration order, then the handler.
r := gorouter.New(gorouter.WithMiddleware(a))r.Use("/", b)r.UseMethod(http.MethodGet, "/", c)r.GET("/", h)// GET / runs a → b → c → hUse covers routes registered before and after the call, so you can group middleware at the top or the bottom of your setup.
Each route’s chain is built once, when a route or middleware is registered, never per request. On net/http, a route behind three no-op middleware takes 24 ns against 20 ns without, and the chain allocates nothing.
A complete example
Section titled “A complete example”Logging, panic recovery, a header on every API response and a check on API request bodies:
// Command middleware shows the three places middleware can go: around the whole router,// around the routes below a path, and around one method below a path.package main
import ( "io" "log" "log/slog" "mime" "net/http" "os" "time"
"github.com/vardius/gorouter/v5")
func newRouter(logger *slog.Logger) *gorouter.Router { // Around the whole router: these also see 404 and 405 responses. r := gorouter.New(gorouter.WithMiddleware(logRequests(logger), recoverPanics(logger)))
// Around every route below /api, on every method. r.Use("/api", noStore)
// Around POST routes below /api only. r.UseMethod(http.MethodPost, "/api", requireJSON)
r.GET("/", index) r.GET("/api/status", status) r.POST("/api/echo", echo) r.GET("/panic", func(http.ResponseWriter, *http.Request) { panic("something broke") })
return r}
func index(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "welcome\n")}
func status(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") io.WriteString(w, `{"status":"ok"}`)}
func echo(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") io.Copy(w, http.MaxBytesReader(w, r.Body, 1<<20))}
// logRequests logs each request's method, path and duration.func logRequests(logger *slog.Logger) gorouter.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) logger.Info("request", "method", r.Method, "path", r.URL.Path, "took", time.Since(start)) }) }}
// recoverPanics turns a panicking handler into a 500 response.func recoverPanics(logger *slog.Logger) gorouter.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { logger.Error("panic", "path", r.URL.Path, "error", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) }}
// noStore keeps API responses out of caches.func noStore(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") next.ServeHTTP(w, r) })}
// requireJSON rejects request bodies that aren't JSON.func requireJSON(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if mediaType, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")); mediaType != "application/json" { http.Error(w, "send JSON", http.StatusUnsupportedMediaType) return } next.ServeHTTP(w, r) })}
func main() { logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) srv := &http.Server{Addr: ":8080", Handler: newRouter(logger), ReadHeaderTimeout: 5 * time.Second} log.Fatal(srv.ListenAndServe())}