gorilla/mux: Regex Routes & Host Matching
gorilla/mux is a mature net/http router that emphasizes explicit route constraints: regex path variables, host-based routing, headers, queries, and schemes.
It shines when URLs carry strict formats (IDs with prefixes, version segments, tenant subdomains) that chi-style segment matching expresses awkwardly.
Summary
mux compiles routes into a matcher list evaluated per request.
Each route can require HTTP methods, hostnames, headers, and query keys in addition to path templates.
Handlers stay stdlib-compatible, but the project is in maintenance mode - choose mux when its matchers solve a real routing problem, not by default for new services.
Recipe
Quick-reference recipe card - copy-paste ready.
r := mux.NewRouter()
r.Host("{tenant}.example.com").
Path("/api/v{version:[0-9]+}/items/{id:[a-z]+}").
Methods(http.MethodGet).
HandlerFunc(getItem)When to reach for this:
- Multi-tenant apps routing by subdomain (
{tenant}.api.example.com) - Paths requiring regex validation at the router (
idmust match[0-9]{6}) - Matching on headers (
Accept, custom auth headers) or query presence - Legacy codebases already standardized on mux
Working Example
package main
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
type item struct {
ID string `json:"id"`
Version int `json:"version"`
Tenant string `json:"tenant"`
}
func getItem(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
out := item{
ID: vars["id"],
Version: mustAtoi(vars["version"]),
Tenant: vars["tenant"],
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(out)
}
func mustAtoi(s string) int {
n, _ := strconv.Atoi(s)
return n
}
func main() {
r := mux.NewRouter()
api := r.Host("{tenant:[a-z0-9-]+}.localhost").Subrouter()
api.HandleFunc("/v{version:[0-9]+}/items/{id:[a-z]{3,}}", getItem).Methods(http.MethodGet)
r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
http.ListenAndServe(":8080", r)
}What this demonstrates:
- Host template capturing tenant slug
- Version segment constrained to digits via regex
- Item ID constrained to lowercase letters with minimum length
mux.Varsmaps named captures to handler logic
Deep Dive
How It Works
NewRoutercreates a root router implementinghttp.Handler.- Routes register matchers; the first fully matching route wins (order matters when patterns overlap).
Subrouter()scopes path prefixes and inherits parent matchers.StrictSlashcontrols trailing slash behavior viar.StrictSlash(true).
Matcher Building Blocks
| Builder | Purpose | Example |
|---|---|---|
Path / PathPrefix | Path template | /users/{id} |
Host | Subdomain or domain | {tenant}.example.com |
Methods | Verb allowlist | GET, POST |
Headers | Required header values | X-API-Key present |
Queries | Query key/value rules | format=json |
Schemes | http vs https | TLS-terminated routing |
Regex in Path Variables
Use {name:pattern} syntax.
{id:[0-9]+} rejects non-numeric IDs before your handler runs, returning 404 for non-matches.
Keep regexes readable - complex patterns belong in documentation and tests.
Go Notes
// Middleware with mux: wrap the router or per-route handlers
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.Info("request", "path", r.URL.Path, "host", r.Host)
next.ServeHTTP(w, r)
})
}
srv := &http.Server{Addr: ":8080", Handler: logging(r)}mux does not ship a rich middleware package like chi; compose stdlib-style wrappers or use alice if you want chaining helpers.
Gotchas
- Maintenance mode - gorilla/mux receives fixes but few features; long-term greenfield projects often pick chi unless matchers are mandatory. Fix: document the matcher requirement in ADRs when choosing mux.
- Route order and overlap - Broader routes registered before specific ones can shadow traffic. Fix: register most specific routes first or use subrouters to isolate domains.
- Regex cost and readability - Heavy regex on hot paths is harder to debug than handler validation. Fix: constrain only what routing must reject early; validate business rules in handlers.
- Host matching behind proxies -
r.Hostmay reflect internal names unlessForwarded/X-Forwarded-Hostis normalized. Fix: terminate TLS at gateway that sets external host, or middleware rewritesr.Hostdeliberately. - Vars absent on wrong route -
mux.Varsreturns nil if matcher names differ from handler expectations. Fix: unit-test each template and centralize var parsing helpers. - StrictSlash surprises - Automatic redirects can confuse API clients that do not follow 301/308. Fix: align client URLs with
StrictSlashpolicy.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| chi | Modern radix routing, active middleware ecosystem | You need host/regex matchers mux provides cleanly |
stdlib ServeMux | Simple {id} patterns in Go 1.22+ | Host/header/query rules are complex |
| Gin/Echo | Framework binding and JSON helpers | You want stdlib handlers and explicit matchers |
| API gateway (nginx, Envoy) | Host/path routing at edge | You need in-process routing only |
FAQs
Is gorilla/mux deprecated?
It is in maintenance mode - safe for existing apps, but evaluate chi or edge routing for new services unless mux matchers are required.
How is mux different from chi URL params?
Both name segments; mux adds inline regex constraints and additional matchers for host, headers, and queries on the same route builder.
Can I use middleware with mux?
Yes - wrap the router or individual handlers using standard func(http.Handler) http.Handler middleware.
How do I test mux routes?
Use httptest with fully qualified paths and Host headers set on the request to exercise host matchers.
What happens when no route matches?
mux returns 404; use r.NotFoundHandler to customize responses and logging.
How do subrouters inherit matchers?
Child subrouters combine parent path prefixes and can add their own Host or Headers constraints.
Should regex validation live in mux or handlers?
Use mux for structural URL shape (digits-only IDs); keep business validation (exists in DB) in handlers/services.
Does mux support CORS?
Not built-in - add middleware setting Access-Control-* headers before your handlers.
How do I migrate from mux to chi?
Rewrite route templates ({id:regex} to {id} or handler validation) and replace mux.Vars with chi.URLParam; keep handlers if already stdlib-shaped.
Can one server listen on multiple hosts?
Yes - host matchers on routes or subrouters dispatch by r.Host on a shared listener.
How does mux interact with TLS?
Configure http.Server TLS as usual; use Schemes("https") when terminating TLS in-process.
Why not use an API gateway instead of host routing in Go?
Edge gateways excel at TLS and coarse routing; in-process host rules help when you want one binary and testable routing without extra infra.
Related
- chi: Lightweight Routing & Middleware - default router choice for many teams
- Go HTTP Frameworks: Routers vs Full Frameworks - router vs framework landscape
- Framework Selection Decision Guide - when mux ranks higher
- Migrating Between Routers Without Rewriting Handlers - stdlib handler portability
- HTTP Servers, Handlers & ServeMux Patterns - stdlib routing baseline
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).