Skip to content

Middleware

Middleware has the shape most Go middleware already has, so existing middleware works as it is:

type Middleware = func(http.Handler) http.Handler

On fasthttp it is func(fasthttp.RequestHandler) fasthttp.RequestHandler.

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

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

Logging, panic recovery, a header on every API response and a check on API request bodies:

examples/middleware/main.go
// 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())
}