njia

package module
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

njia - A fast, zero-dependency HTTP router for Go

CI Benchmarks Go Reference Go License

Native routing for modern Go applications, plus a drop-in compatibility layer for migrating from gorilla/mux.

njia (Swahili: path, way) is an HTTP router built around three principles:

  • Fast request matching
  • Zero dependencies
  • Easy migration from gorilla/mux

Whether you're building a REST API, reverse proxy, API gateway, or platform, Njia provides a modern router that is fast, introspectable, and safe to use in production.

The main module has zero require entries and imports only the Go standard library. gorilla/mux is used exclusively in a separate test module (internal/difftest) to verify behavioral compatibility.

Installation

go get github.com/jkaninda/njia

For gorilla/mux compatibility:

go get github.com/jkaninda/njia/muxcompat

Quickstart

package main

import (
    "log"
    "net/http"

    "github.com/jkaninda/njia"
)

func main() {
    r := njia.New()

    r.GET("/healthz", healthHandler)
    r.GET("/users/{id}", getUser)
    r.GET("/files/{rest...}", serveFile)

    api := r.Group(
        "/api/v1",
        authMiddleware,
        rateLimitMiddleware,
    )

    api.POST("/orders", createOrder)

    log.Fatal(http.ListenAndServe(":8080", r))
}

Read a parameter without building a map:

func getUser(w http.ResponseWriter, r *http.Request) {
    id := njia.Param(r, "id")   // no map, no allocation
    ...
}

Routing

Registering routes

GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS are shorthands for Handle, which takes any method:

r.Handle("REPORT", "/calendars/{id}", reportHandler)
r.HandleFunc("PURGE", "/cache/{key...}", purge)

A route registered for GET also answers HEAD, and the Allow header of a 405 lists HEAD alongside GET accordingly.

Every method: ANY

ANY registers one handler for every method, including verbs no RFC names:

r.ANY("/api/{rest...}", proxy)

This is what a reverse proxy needs. A gateway forwards whatever verb the client sent — WebDAV's PROPFIND, a vendor's custom verb — and lets the backend decide what it accepts; enumerating methods at registration time would make the router reject a request the proxy would have been happy to forward.

An explicitly registered method always wins over the wildcard, so one route can serve a verb specially and proxy the rest:

r.GET("/files/{path...}", readFromCache)  // GET comes from here
r.ANY("/files/{path...}", proxy)          // everything else from here

A path served by ANY never answers 405, because there is no method it rejects. The * sentinel is a registration detail and never appears in an Allow header.

Options
api.POST("/orders", createOrder, njia.WithName("createOrder"))
Option Effect
WithName(name) Names the route; must be unique. Retrieve with Router.Route(name).
WithMeta(key, value) Attaches an arbitrary annotation, surfaced by Routes().
WithHost(patterns...) Restricts this route to host patterns, overriding its group's.
WithMiddleware(mw...) Wraps only this route, inside any group middleware.
WithPriority(n) Orders this route ahead of specificity; lower first. See Match order.

Registration returns an error instead of panicking — see Errors.

Parameters

{id} matches any single non-empty segment. {rest...} absorbs the remainder of the path including slashes, and must be the last segment. A placeholder carries a name and nothing else.

Constraints are rejected, not ignored
err := r.GET("/users/{id:int}", getUser)
// njia: route GET "/users/{id:int}": njia: malformed route pattern:
// "{id:int}" constrains "id", which this router does not support; write {id}

A pattern that looks like it filters values can never silently match everything. Validate in the handler, where a bad value produces a useful 400 rather than falling through to a 404. If you need matching itself to depend on the value, muxcompat accepts gorilla's {id:[0-9]+}.

Reading captured values
Call Returns Allocates
Param(r, "id") The value, or "". no
ParamAt(r, i) (name, value, ok) in pattern order. no
NumParams(r) How many were captured. no
AppendParams(r, dst) Appends every parameter to dst. no, given capacity
ParamMap(r) map[string]string, the shape gorilla's Vars returned. yes
RouteOf(r) The *Route that matched. no

SetParams(req, params...) attaches parameters to a request, for tests that invoke a handler directly rather than through the router.

Parameters are captured into a fixed-size array carried by the request context and spill to the heap only past that size. A static route declares no parameters and so writes nothing to the context at all.

Match order

Routes are matched most-specific-first: a static segment beats a wildcard, which beats a catch-all, resolved position by position from the left. Matching backtracks, so a static branch that dead-ends never hides a wildcard that would have matched.

Host specificity is considered after path specificity — see Host matching.

Overriding it: WithPriority

Specificity is the right default — given /api/v1/{rest...} and /api/{rest...}, the longer prefix is almost always what should serve /api/v1/x. A gateway assembling routes from user configuration sometimes needs the opposite, and specificity alone cannot express it:

r.ANY("/api/{rest...}", maintenance, njia.WithPriority(-1))
r.ANY("/api/v1/{rest...}", backend)
// GET /api/v1/x -> maintenance, despite being the less specific pattern

Priority is compared before specificity, lower first. DefaultPriority is 0, so a negative value pulls a route ahead of everything unmarked and a positive one pushes it behind.

Two details worth knowing:

  • Routes sharing a path pattern share the lowest priority any of them asked for, because ordering picks a pattern before it picks a method.
  • Priorities disable the lookup fast paths for the whole table, since those answer with the most specific match without consulting other candidates. Leaving priority unset everywhere — the default — costs nothing.
Groups and middleware

A group prefixes patterns and wraps handlers, and nests:

api := r.Group("/api/v1", authMiddleware, rateLimitMiddleware)
v1u := api.Group("/users", auditMiddleware)
v1u.GET("/{id}", getUser)                    // GET /api/v1/users/{id}

api.Prefix()                                 // "/api/v1"
api.Hosts()                                  // host patterns, if restricted

Order, outermost first: router middleware, then each enclosing group's middleware from outer to inner, then the route's own WithMiddleware, then the handler. This is tested, not merely documented.

Use is not positional

Middleware is resolved when the table is compiled, not when a route is registered, so where a Use call sits among the registrations does not change what it covers. A route registered before it is wrapped just like one registered after, and so is a child group created before it:

api := r.Group("/api")
v1 := api.Group("/v1")       // created before the Use
api.Use(auth)                // covers v1 as well
v2 := api.Group("/v2")       // and v2

v1.GET("/orders", list)      // authenticated
v2.GET("/orders", list)      // authenticated

Router.Use and Group.Use behave the same way; the only difference is scope. This matches gorilla/mux, whose middleware ran at match time and so applied whatever the registration order — code moved across keeps working, and moving a Use call up or down a file can never silently drop authentication.

To wrap only some routes, say so structurally rather than by ordering:

api := r.Group("/api")
api.GET("/public", public)                    // no auth

secure := api.Group("/admin", auth)           // scope is visible here
secure.GET("/settings", settings)

or attach it to a single route with WithMiddleware.

Mounting a handler

Mount hands every request at or below a prefix to one handler, for every method — another router, a file server, a debug endpoint:

r.Mount("/debug/pprof", pprofHandler)
r.Mount("/static", http.FileServer(http.Dir("public")))
r.Mount("/legacy", oldRouter)

Both the prefix and its subtree are covered, so /static and /static/css/app.css both reach the handler, and matching is segment-bounded: mounting /api does not capture /apiary.

The prefix is not stripped. A proxy needs the path as it arrived, and a handler that wants it removed can say so, which reads better than a routing rule that silently rewrites:

r.Mount("/static", http.StripPrefix("/static", fs))

A more specific route still wins, which is how an exception is carved out of a mount:

r.Mount("/api", proxy)
r.GET("/api/health", localHealth)   // served locally, not proxied

The remainder is captured under MountParam. Because that name is fixed, mounting and separately registering a differently named catch-all at the same position conflict — r.Mount("/admin", h) then r.GET("/admin/{files...}", x) returns ErrParamConflict. A plain {id} parameter there is fine.

Router configuration

Every field is off by default. Set them on a router from New(), which is the only supported way to construct one — the zero Router has no route table and panics on registration.

r := njia.New()
r.NotFound = http.HandlerFunc(myNotFound)
r.MethodNotAllowed = http.HandlerFunc(my405)
r.CleanPath = true
r.RedirectTrailingSlash = true
r.RouteInContext = true
Field Effect when set
NotFound Serves unmatched requests. Default: http.NotFoundHandler.
MethodNotAllowed Serves a path hit with the wrong method. Default: 405 plus an Allow header.
CleanPath Redirects a non-canonical path to its cleaned form with 301 — /a//b/a/b.
RedirectTrailingSlash Redirects /x/ to /x, or /x to /x/, when only the other is registered. Without it, the other form is a 404.
RouteInContext Makes RouteOf work for static routes too, at one allocation per request.

RouteInContext exists because a static route otherwise attaches nothing to the request. Leave it off unless handlers actually need the matched route.


Host matching

gw := r.Host("api.example.com", "*.api.example.com")
gw.GET("/orders/{id}", getOrder)

r.Host("{tenant}.app.example.com").GET("/dashboard", dashboard)  // njia.Param(req, "tenant")
r.GET("/healthz", health)                                        // every host
Patterns

Most specific first:

Pattern Matches
api.example.com:8443 exactly this host on this port
api.example.com exactly this host, any port
{sub}.example.com exactly one leading label, captured as sub
*.example.com one or more leading labels
{sub...}.example.com one or more leading labels, captured
{host...} any host, captured whole
* any host — also spelled njia.AnyHost

Matching is case-insensitive and ignores a trailing dot, so API.Example.COM. and api.example.com are the same name. A pattern that names a port only matches requests carrying that port; one that does not, ignores the port entirely.

WithHost(...) restricts a single route and overrides its group. ValidateHost checks a pattern without registering anything.

A host parameter is reported by Routes() before any path parameter, marked InHost with a Position of -1, and is read with njia.Param like any other.

Precedence

Path specificity is decided first, host specificity second, registration order last. A global /healthz therefore stays reachable underneath a per-host catch-all proxy route — which is exactly how a gateway needs it:

r.GET("/healthz", health)                                  // wins on every host
r.Host("okapi.example.com").GET("/{rest...}", proxyOkapi)  // everything else

Within one path pattern, hosts are consulted from most to least specific, and a variant that does not serve the request's method falls through to a less specific one.

  • A path that exists but not on the requested host is a 404.
  • A path that exists on that host but not for that method is a 405, and the Allow header only lists methods that host actually serves.
Cost

Exact hosts are indexed by name, so a gateway with a thousand virtual hosts costs one map lookup, not a thousand comparisons. Tables that use no host constraint at all never read the request's host — the feature costs them nothing.


Errors

Registration returns errors and never panics:

if err := r.GET("/users/{id", handler); err != nil {
    // njia: route GET "/users/{id": njia: malformed route pattern: ...
}

This matters for gateways that build routes from user-supplied configuration: a bad entry is rejected, not fatal. Paired with Swap, a whole table is validated off to the side and installed only if it is sound, so a typo in someone's YAML can never take the process down.

Routes fixed in code can be checked in one place rather than at every call, with Builder.Err() after registering, or by letting a bad table fail the Swap.

Sentinels

Every failure is a typed sentinel wrapped in a *RouteError that names the offending method and pattern.

Sentinel Cause
ErrBadPattern Malformed template.
ErrNoLeadingSlash Pattern does not start with /.
ErrDuplicateRoute Same method and pattern registered twice.
ErrDuplicateName Two routes given the same name.
ErrParamConflict Conflicting parameter names at one position.
ErrCatchAllPosition {rest...} is not the last segment.
ErrNoHandler Route registered without a handler.
ErrEmptyMethod Route registered without a method.
ErrBadHost Malformed host pattern.
*RouteError

Exposes Method, Pattern and Err, and implements Unwrap:

if errors.Is(err, njia.ErrDuplicateRoute) {
    ...
}

var rerr *njia.RouteError
if errors.As(err, &rerr) {
    log.Printf("bad route %s %s: %v", rerr.Method, rerr.Pattern, rerr.Err)
}

Builder accumulates errors so a gateway can report every problem in a configuration file rather than only the first — see Hot reload. ValidateHost checks a host pattern without registering anything, so a bad configuration entry can be rejected before a table is built.


Introspection

for _, ri := range r.Routes() {
    fmt.Println(ri.Method, ri.PathTemplate, ri.Params, ri.Meta)
}

route := r.Route("createOrder")   // by name
fmt.Println(r.String())           // whole table, for start-up logs

RouteInfo carries Name, Method, PathTemplate as written, Hosts, Params, the Handler before middleware, and any Meta annotations. Each ParamInfo gives Name, Position, CatchAll and InHost.

An OpenAPI generator needs nothing else — in particular it never has to reconstruct a template from a compiled regular expression. Value types are not part of the pattern, so a generator carries schema information in WithMeta.

A *Route obtained from Route(name) or RouteOf(req) exposes the same information through Method(), Pattern(), Hosts(), Name(), Handler(), Params() and Meta(key).

Matching without serving
route, ok := r.Lookup(req)                      // no allocation, no parameters

var buf [8]njia.PathParam
route, params, ok := r.LookupInto(req, buf[:0]) // no allocation, with parameters

Useful for authorization checks, metrics labelled by route template, and anything that needs to know which route would serve a request without serving it.


Hot reload

err := r.Swap(func(b *njia.Builder) error {
    for _, route := range configFromYAML() {
        if err := b.Handle(route.Method, route.Path, route.Handler); err != nil {
            return err
        }
    }
    return nil
})

The new table is built and fully validated off to the side. On any error the running table is untouched. On success it is installed with a single atomic pointer store; in-flight requests finish against the old table and there is no lock anywhere on the request path.

Builder

A Builder can also be built and inspected on its own, which lets a gateway report every problem in a configuration file rather than only the first:

b := njia.NewBuilder()
for _, route := range configFromYAML() {
    _ = b.Handle(route.Method, route.Path, route.Handler)
}
if errs := b.Errs(); len(errs) > 0 {
    return fmt.Errorf("%d bad routes: %w", len(errs), b.Err())
}

Builder carries the same registration surface as RouterUse, Group, Host, Handle, HandleFunc and the method shorthands.


Migrating from gorilla/mux

gorilla/mux was archived in December 2022 and has been effectively dormant since. muxcompat lets a project move off it with an import rewrite:

-import "github.com/gorilla/mux"
+import mux "github.com/jkaninda/njia/muxcompat"

Nothing else changes. The package reproduces gorilla's exported API and its observable behavior — route ordering, strict-slash redirects, path cleaning, MatchErr propagation, subrouter matcher inheritance, reverse URL building, Walk, CORSMethodMiddleware.

Including the surprising corners
  • Queries with an odd number of arguments records an error and returns nil, so chaining onto it panics. Reproduced, because callers may depend on it.
  • A host template without a port has the request's port stripped at the first colon; a host template with a port does not.
  • Methods() with no arguments matches nothing.
  • Queries("k", "") matches the key with any value.
  • A capturing group inside a variable pattern panics at registration.

Where njia deliberately differs, it is only by being more robust: a handful of inputs make gorilla fault at runtime (nil pointer dereference, slice bounds out of range) and njia serves them instead. The differential harness treats a gorilla runtime fault as a gorilla bug and only requires that njia does not fault differently.

Not the destination

muxcompat is a bridge. It stays published for anyone migrating off gorilla, but new features go into the native API. Nothing in muxcompat imports the root njia package; the two surfaces evolve independently on top of shared internal/ packages.


Correctness

Behavior is never written from memory or from documentation prose. Every gorilla behavior njia reproduces was first observed by running real gorilla.

  • internal/difftest drives both engines with identical route tables and identical requests, then compares the matched route, captured variables, response status, redirect location, response body, match error, per-route build errors and panic behavior.
  • internal/difftest/vendored is gorilla's own test suite, adapted to target muxcompat. It carries gorilla's BSD-3-Clause header; only test cases and fixtures were taken, never implementation code. OMITTED.md records the handful of white-box tests that could not be expressed through the exported API.
  • A property-based generator builds random route tables and request paths covering static paths, wildcards, regular expression constraints, prefixes, host templates, methods, queries, headers, schemes, subrouters, overlapping routes, percent-encoded and empty and dot segments, very long paths and unicode. CI runs 200,000 generated cases per commit.
  • Real route tables extracted from Okapi and Goma Gateway are replayed against both engines under five router configurations. This is the migration acceptance gate.
  • The lookup index is proved inert: muxcompat can be forced onto the plain ordered scan, and a test drives every table both ways and requires the two to agree on every observable field.
  • Host routing is checked against a reference model: a deliberately naive resolver that scans every route and sorts, run against 400 generated route tables over every combination of 11 hosts, 13 paths and 5 methods — about 286,000 comparisons. It is what caught the specificity bug that let /api/{rest...} shadow /api.
  • Allocation counts are asserted, not hoped for. Dedicated tests require a static match to serve with zero allocations and the tree lookup to capture parameters without allocating, so a regression fails the build rather than quietly showing up in a benchmark later.

Performance

The native router matches with a segment-indexed prefix tree and a direct map lookup for fully static patterns. muxcompat splits its table: routes that are static segments plus plain {name} wildcards with at most a method filter go into the tree, everything else stays on an ordered scan, and registration sequence numbers are compared across the two so gorilla's first-registered-wins ordering is preserved exactly.

internal/difftest/bench compares gorilla/mux, the standard library ServeMux, chi and both njia surfaces at 10, 100 and 1000 routes, across static hits, parameter hits, deep nested hits, 404 misses, 405 mismatches, virtual-host routing and table registration.

Run the grid yourself:

cd internal/difftest
go test -run '^$' -bench . -benchmem -count=6 ./bench/...
go run golang.org/x/perf/cmd/benchstat@latest -col /size <output>
Feature costs

BenchmarkGateway_* measures the two proxy features against the same table registered without them. These are native-only and sit outside the cross-engine grid, because gorilla has no equivalent of either and a comparison against an engine doing something different says nothing.

go test -run '^$' -bench 'BenchmarkGateway_' -benchmem -count=6 ./bench/...

Unlike the grid, both sides of each comparison come from one binary, so the alignment noise described below does not apply and the deltas are readable directly. Two properties are worth confirming on your own hardware.

Reading a run
  • -count=6 and benchstat are not optional ceremony. A single pass is noisy enough that machine drift reads as a real regression.
  • A delta under roughly 6% is not attributable. When comparing two njia revisions, read the gorilla, stdlib and chi rows first. Their code is identical between revisions, yet they still move by several percent, because two different njia binaries shift code alignment around unrelated functions. That floor is a property of the binaries, not of the machine, so no amount of repetition or interleaving removes it. Promote a delta to real only if it clears the band and is corroborated — the Lookup/* rows, which measure matching without ServeHTTP, are a good independent check on the Router_* rows.

The grid also contains TestGridSanity and TestHostGridSanity, which assert that every engine really returns 200/404/405 for the scenarios it is benchmarked on, so no router can look fast by quietly 404ing.


License

Apache-2.0. Test cases and fixtures adapted from gorilla/mux are BSD-3-Clause and retain their original copyright header; see NOTICE.

Documentation

Overview

Package njia is a zero-dependency HTTP router for Go.

It routes with a segment-indexed prefix tree, captures path parameters without building a map, and reports its own route table so that documentation can be generated from the routes themselves rather than reconstructed from compiled regular expressions.

r := njia.New()
r.GET("/users/{id}", http.HandlerFunc(getUser))

api := r.Group("/api/v1", authMiddleware)
api.GET("/orders/{id}", http.HandlerFunc(getOrder))

Registration returns an error instead of panicking, and Swap replaces the whole route table atomically after validating it, so a process that builds routes from user-supplied configuration can reload without ever going down.

Proxies and gateways

Two features exist for reverse proxies, which route on behalf of backends rather than serving their own handlers.

ANY registers one handler for every HTTP method, including verbs no RFC names, so a proxy can forward whatever the client sent instead of rejecting anything not enumerated at registration time:

r.ANY("/api/{rest...}", proxy)

WithPriority orders a route against the others that match the same path, ahead of specificity, so a catch-all can deliberately shadow a more specific route:

r.ANY("/api/{rest...}", maintenance, njia.WithPriority(-1))

Mount hands a whole subtree to one handler, which is how another router, a file server or a debug endpoint is attached to a path. The prefix is not stripped:

r.Mount("/debug/pprof", pprofHandler)

Middleware ordering

Middleware is resolved when the table is compiled, not when a route is registered, so Use is not positional: it covers everything in its scope whatever the order. Router.Use and Group.Use behave alike, differing only in what they cover, and a route or child group that already existed is wrapped just as one created afterwards.

api := r.Group("/api")
v1 := api.Group("/v1")
v1.GET("/orders", listOrders)
api.Use(auth)              // covers /api/v1/orders too

To wrap only some routes, nest a group or attach the middleware to the route with WithMiddleware, so the scope is visible in the structure rather than dependent on where a line sits.

Applied outermost first: router middleware, then each enclosing group from the outside in, then the route's own, then the handler.

Package github.com/jkaninda/njia/muxcompat is a separate, drop-in replacement for github.com/gorilla/mux, for projects migrating away from it.

Index

Constants

View Source
const AnyHost = "*"

AnyHost is the host pattern that matches every request. Writing it out is equivalent to registering a route with no host constraint at all, and it exists so that a configuration file can express "any host" explicitly.

View Source
const DefaultPriority = 0

DefaultPriority is the priority a route has when none is given. Priorities are compared with lower first, so a route can be pulled ahead of the default with a negative value as well as pushed behind it with a positive one.

View Source
const MethodAny = "*"

MethodAny registers a handler for every HTTP method, including methods this package has no named helper for and methods that are not in any RFC.

It is what a reverse proxy needs: a gateway forwards whatever verb the client sent — WebDAV's PROPFIND, a gRPC-Web POST, a vendor's custom verb — and decides for itself which ones a backend accepts. Enumerating the methods at registration time would make the router reject a verb the proxy would have been happy to forward.

A method registered explicitly always wins over the wildcard, so a route can serve GET from one handler and everything else from another:

r.GET("/files/{path...}", readOnly)
r.ANY("/files/{path...}", proxy)

A path served by a wildcard never answers 405, because there is no method it does not accept.

View Source
const MountParam = "mount"

MountParam is the name of the parameter a mounted subtree captures the remainder of the path into. It is reported by Route.Params like any other, so a mounted handler can read the unmatched remainder with Param(r, MountParam) when it wants it.

Because it is a fixed name, mounting under a prefix and separately registering a differently named catch-all at the same position conflict:

r.Mount("/admin", h)              // registers /admin/{mount...}
r.GET("/admin/{files...}", other) // ErrParamConflict: position already named

Registering a plain {name} parameter there is fine; only two catch-alls at one position collide.

Variables

View Source
var (
	// ErrBadPattern reports a template that could not be parsed, such as one
	// with unbalanced braces or an empty variable name.
	ErrBadPattern = errors.New("njia: malformed route pattern")
	// ErrNoLeadingSlash reports a pattern that does not start with a slash.
	ErrNoLeadingSlash = errors.New("njia: route pattern must start with a slash")
	// ErrDuplicateRoute reports a method and pattern pair that is already
	// registered.
	ErrDuplicateRoute = errors.New("njia: duplicate route")
	// ErrParamConflict reports two routes that put differently named
	// parameters with the same constraint at the same position.
	ErrParamConflict = errors.New("njia: conflicting parameter name")
	// ErrCatchAllPosition reports a catch-all parameter that is not the last
	// segment of the pattern.
	ErrCatchAllPosition = errors.New("njia: catch-all parameter must be last")
	// ErrDuplicateName reports a route name that is already in use.
	ErrDuplicateName = errors.New("njia: duplicate route name")
	// ErrNoHandler reports a route registered without a handler.
	ErrNoHandler = errors.New("njia: route has no handler")
	// ErrEmptyMethod reports a route registered without an HTTP method.
	ErrEmptyMethod = errors.New("njia: route has no method")
	// ErrBadHost reports a host pattern that could not be parsed.
	ErrBadHost = errors.New("njia: malformed host pattern")
)

Errors reported by route registration. Every registration failure is one of these, wrapped in a *RouteError naming the route it came from; nothing in this package panics.

Functions

func NumParams

func NumParams(r *http.Request) int

NumParams returns how many path parameters the matched route captured.

func Param

func Param(r *http.Request, name string) string

Param returns the value of the named path parameter, or the empty string if the request has no such parameter.

It builds no map and reads no more than the parameters the matched route declared.

func ParamAt

func ParamAt(r *http.Request, i int) (name, value string, ok bool)

ParamAt returns the i'th path parameter in pattern order. It reports false when i is out of range.

func ParamMap

func ParamMap(r *http.Request) map[string]string

ParamMap returns the captured parameters as a map. It allocates, and exists for callers that need the shape gorilla's Vars returned.

func SetParams

func SetParams(r *http.Request, params ...PathParam) *http.Request

SetParams attaches path parameters to a request. It is intended for tests that invoke a handler directly.

func ValidateHost

func ValidateHost(pattern string) error

ValidateHost reports whether a host pattern is well formed, without registering anything. A gateway that builds its route table from user configuration can use it to reject a bad entry early, with the same error it would get from registration.

Accepted forms, in decreasing specificity:

api.example.com          exactly this host
api.example.com:8443     exactly this host on this port
{sub}.example.com        exactly one leading label, captured as "sub"
*.example.com            one or more leading labels
{sub...}.example.com     one or more leading labels, captured as "sub"
{host...}                any host, captured whole
*                        any host

Matching is case-insensitive and ignores a trailing dot. A pattern that names a port only matches requests carrying that port; a pattern that does not accepts any port.

Types

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

Builder accumulates a route table. It is what Swap hands to its callback, and it is also the object a Router registers into.

Every registration is validated as it happens, so an invalid table is rejected before it can be installed.

func NewBuilder

func NewBuilder() *Builder

NewBuilder returns an empty builder. A table can be assembled and fully validated with one, then installed on a router with Swap.

func (*Builder) ANY added in v0.0.2

func (b *Builder) ANY(pattern string, h http.Handler, opts ...RouteOption) error

ANY registers a handler for every HTTP method. See MethodAny.

func (*Builder) DELETE

func (b *Builder) DELETE(pattern string, h http.Handler, opts ...RouteOption) error

DELETE registers a handler for DELETE requests.

func (*Builder) Err

func (b *Builder) Err() error

Err returns the first registration error recorded on the builder, so that a caller which ignored individual return values can still check the table as a whole.

func (*Builder) Errs

func (b *Builder) Errs() []error

Errs returns every registration error recorded on the builder.

func (*Builder) GET

func (b *Builder) GET(pattern string, h http.Handler, opts ...RouteOption) error

GET registers a handler for GET requests.

func (*Builder) Group

func (b *Builder) Group(prefix string, mw ...Middleware) *Group

Group returns a group rooted at the builder.

func (*Builder) HEAD

func (b *Builder) HEAD(pattern string, h http.Handler, opts ...RouteOption) error

HEAD registers a handler for HEAD requests.

func (*Builder) Handle

func (b *Builder) Handle(method, pattern string, h http.Handler, opts ...RouteOption) error

Handle registers a handler for a method and pattern.

func (*Builder) HandleFunc

func (b *Builder) HandleFunc(method, pattern string, h http.HandlerFunc, opts ...RouteOption) error

HandleFunc registers a handler function for a method and pattern.

func (*Builder) Host

func (b *Builder) Host(patterns ...string) *Group

Host returns a group rooted at the builder whose routes only answer on the given host patterns.

func (*Builder) Mount added in v0.0.3

func (b *Builder) Mount(prefix string, h http.Handler, opts ...RouteOption) error

Mount hands every request at or below a prefix to one handler. See Router.Mount.

func (*Builder) OPTIONS

func (b *Builder) OPTIONS(pattern string, h http.Handler, opts ...RouteOption) error

OPTIONS registers a handler for OPTIONS requests.

func (*Builder) PATCH

func (b *Builder) PATCH(pattern string, h http.Handler, opts ...RouteOption) error

PATCH registers a handler for PATCH requests.

func (*Builder) POST

func (b *Builder) POST(pattern string, h http.Handler, opts ...RouteOption) error

POST registers a handler for POST requests.

func (*Builder) PUT

func (b *Builder) PUT(pattern string, h http.Handler, opts ...RouteOption) error

PUT registers a handler for PUT requests.

func (*Builder) Use

func (b *Builder) Use(mw ...Middleware)

Use appends router-level middleware, applied outermost first to every route in the table.

type Group

type Group struct {
	// contains filtered or unexported fields
}

Group is a path prefix and a middleware chain that routes are registered against.

Middleware ordering is fixed and tested: router-level middleware is outermost, then each enclosing group's middleware from outer to inner, then the route's own middleware, then the handler.

func (*Group) ANY added in v0.0.2

func (g *Group) ANY(pattern string, h http.Handler, opts ...RouteOption) error

ANY registers a handler for every HTTP method. See MethodAny.

func (*Group) DELETE

func (g *Group) DELETE(pattern string, h http.Handler, opts ...RouteOption) error

DELETE registers a handler for DELETE requests.

func (*Group) GET

func (g *Group) GET(pattern string, h http.Handler, opts ...RouteOption) error

GET registers a handler for GET requests.

func (*Group) Group

func (g *Group) Group(prefix string, mw ...Middleware) *Group

Group returns a nested group.

The child records its parent rather than copying its middleware, so middleware added to any enclosing group afterwards still reaches this child's routes.

func (*Group) HEAD

func (g *Group) HEAD(pattern string, h http.Handler, opts ...RouteOption) error

HEAD registers a handler for HEAD requests.

func (*Group) Handle

func (g *Group) Handle(method, pattern string, h http.Handler, opts ...RouteOption) error

Handle registers a handler for a method and pattern within the group.

func (*Group) HandleFunc

func (g *Group) HandleFunc(method, pattern string, h http.HandlerFunc, opts ...RouteOption) error

HandleFunc registers a handler function for a method and pattern.

func (*Group) Host

func (g *Group) Host(patterns ...string) *Group

Host returns a group whose routes only answer on the given host patterns.

The patterns replace, rather than narrow, any host constraint the group already carried: a nested Host call declares outright which hosts its subtree serves. A route matches when any one of the patterns matches.

gw := r.Host("api.example.com", "*.api.example.com")
gw.GET("/orders/{id}", getOrder)

See ParseHostPattern for the accepted forms.

func (*Group) Hosts

func (g *Group) Hosts() []string

Hosts returns the host patterns the group restricts its routes to.

func (*Group) Mount added in v0.0.3

func (g *Group) Mount(prefix string, h http.Handler, opts ...RouteOption) error

Mount hands every request at or below a prefix, resolved against the group's own prefix, to one handler. See Router.Mount.

func (*Group) OPTIONS

func (g *Group) OPTIONS(pattern string, h http.Handler, opts ...RouteOption) error

OPTIONS registers a handler for OPTIONS requests.

func (*Group) PATCH

func (g *Group) PATCH(pattern string, h http.Handler, opts ...RouteOption) error

PATCH registers a handler for PATCH requests.

func (*Group) POST

func (g *Group) POST(pattern string, h http.Handler, opts ...RouteOption) error

POST registers a handler for POST requests.

func (*Group) PUT

func (g *Group) PUT(pattern string, h http.Handler, opts ...RouteOption) error

PUT registers a handler for PUT requests.

func (*Group) Prefix

func (g *Group) Prefix() string

Prefix returns the group's accumulated path prefix.

func (*Group) Use

func (g *Group) Use(mw ...Middleware)

Use appends middleware to this group and everything nested inside it.

Like Router.Use, it applies to every route in its scope whatever the order: routes registered before the call are covered as well as routes registered after, and so are child groups created before it. Middleware is resolved when the table is compiled, not when a route is registered.

api := r.Group("/api")
v1 := api.Group("/v1")
v1.GET("/orders", listOrders)
api.Use(auth)               // covers /api/v1/orders too

To scope middleware to some routes and not others, nest a group or attach it to the route with WithMiddleware. Where the call sits among the registrations does not change what it covers.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware wraps a handler, returning the handler to invoke in its place.

type ParamInfo

type ParamInfo struct {
	// Name is the parameter name.
	Name string
	// Position is the index of the segment the parameter occupies, counting
	// from zero after the leading slash.
	Position int
	// CatchAll reports whether the parameter absorbs the remainder of the
	// path, including slashes.
	CatchAll bool
	// InHost reports that the parameter is captured from the request host
	// rather than from its path. A host parameter is always reported first and
	// has a Position of -1.
	InHost bool
}

ParamInfo describes a parameter declared by a route pattern.

type PathParam

type PathParam struct {
	// Name is the parameter name from the route pattern.
	Name string
	// Value is the text captured from the request path.
	Value string
}

PathParam is a captured path parameter.

func AppendParams

func AppendParams(r *http.Request, dst []PathParam) []PathParam

AppendParams appends every captured parameter to dst and returns the extended slice. It is the allocation-free way to enumerate parameters.

type Route

type Route struct {
	// contains filtered or unexported fields
}

Route is a registered method and pattern pair together with the handler that serves it.

func RouteOf

func RouteOf(r *http.Request) *Route

RouteOf returns the route that matched the request, or nil. A static route is reported even though it attaches no parameters.

func (*Route) Handler

func (r *Route) Handler() http.Handler

Handler returns the handler the route was registered with, before middleware.

func (*Route) Hosts

func (r *Route) Hosts() []string

Hosts returns the host patterns the route is restricted to, as written. It returns nil when the route answers on any host.

func (*Route) Meta

func (r *Route) Meta(key string) (any, bool)

Meta returns the annotation stored under key.

func (*Route) Method

func (r *Route) Method() string

Method returns the HTTP method the route answers.

func (*Route) Name

func (r *Route) Name() string

Name returns the route's name, or the empty string.

func (*Route) Params

func (r *Route) Params() []ParamInfo

Params returns the parameters the pattern declares, in order.

func (*Route) Pattern

func (r *Route) Pattern() string

Pattern returns the route template as written.

func (*Route) Priority added in v0.0.2

func (r *Route) Priority() int

Priority returns the route's matching priority. Lower sorts first, and DefaultPriority is what a route carries when none was set.

type RouteError

type RouteError struct {
	// Method is the HTTP method of the route that failed, if known.
	Method string
	// Pattern is the template of the route that failed.
	Pattern string
	// Err is the underlying cause.
	Err error
}

RouteError identifies which route a registration error came from, so that a gateway loading routes from user configuration can report the offending entry rather than just the failure.

func (*RouteError) Error

func (e *RouteError) Error() string

Error implements the error interface.

func (*RouteError) Unwrap

func (e *RouteError) Unwrap() error

Unwrap returns the underlying cause.

type RouteInfo

type RouteInfo struct {
	// Name is the route's name, or the empty string.
	Name string
	// Method is the HTTP method the route answers.
	Method string
	// PathTemplate is the pattern as written, for example "/users/{id}".
	PathTemplate string
	// Hosts are the host patterns the route is restricted to, as written. It is
	// nil when the route answers on any host.
	Hosts []string
	// Params describes the pattern's parameters in order.
	Params []ParamInfo
	// Handler is the handler the route was registered with, before middleware.
	Handler http.Handler
	// Meta carries the route's user annotations.
	Meta map[string]any
	// Priority is the route's matching priority, lower first. It is
	// DefaultPriority unless WithPriority set it.
	Priority int
}

RouteInfo is a snapshot of a registered route, suitable for generating documentation such as an OpenAPI specification without reconstructing anything from regular expressions.

type RouteOption

type RouteOption func(*Route)

RouteOption customises a route at registration time.

func WithHost

func WithHost(patterns ...string) RouteOption

WithHost restricts a single route to the given host patterns, overriding whatever host constraint its group carried. See ParseHostPattern for the accepted forms.

func WithMeta

func WithMeta(key string, value any) RouteOption

WithMeta attaches an arbitrary annotation to the route. Annotations are reported by Routes and are how a documentation generator carries summaries, tags or schemas alongside a route.

func WithMiddleware

func WithMiddleware(mw ...Middleware) RouteOption

WithMiddleware returns a RouteOption that wraps only this route, inside any group middleware.

func WithName

func WithName(name string) RouteOption

WithName gives the route a name, which must be unique within the router.

func WithPriority added in v0.0.2

func WithPriority(p int) RouteOption

WithPriority orders a route against the others whose patterns also match a request, ahead of specificity. Lower sorts first.

Specificity is the right default: given "/api/v1/{rest...}" and "/api/{rest...}", the longer prefix is almost always the one meant to serve "/api/v1/x". A gateway loading routes from user configuration sometimes needs the opposite — a catch-all that deliberately shadows a more specific route during a migration, say — and specificity alone cannot express that.

r.ANY("/api/{rest...}", maintenance, njia.WithPriority(-1))
r.ANY("/api/v1/{rest...}", backend)

Here the maintenance handler wins for every path under /api despite being the less specific pattern.

Routes that share a path pattern share its priority: the lowest one any of them asks for applies to all, because priority orders patterns against each other and a pattern is matched before a method is chosen. Leaving priority unset everywhere costs nothing and keeps pure specificity ordering.

type Router

type Router struct {
	// NotFound serves requests that matched no route. When nil,
	// http.NotFoundHandler is used.
	NotFound http.Handler

	// MethodNotAllowed serves requests whose path matched a route but whose
	// method did not. When nil, a handler writing 405 with an Allow header is
	// used.
	MethodNotAllowed http.Handler

	// CleanPath makes the router redirect a request whose path is not in
	// canonical form to the cleaned form, with 301.
	CleanPath bool

	// RedirectTrailingSlash makes the router redirect "/x/" to "/x", and
	// "/x" to "/x/", when only the other form is registered.
	RedirectTrailingSlash bool

	// RouteInContext makes the router attach the matched route to the request
	// even when the route declares no parameters, so that RouteOf works for
	// static routes too. It costs one allocation per request; leave it off if
	// handlers do not need it.
	RouteInContext bool
	// contains filtered or unexported fields
}

Router dispatches requests to registered handlers.

A router is safe for concurrent use once it is serving. Registering routes while requests are in flight is not: use Swap, which validates a new table off to the side and installs it atomically.

func New

func New() *Router

New returns an empty router.

func (*Router) ANY added in v0.0.2

func (r *Router) ANY(pattern string, h http.Handler, opts ...RouteOption) error

ANY registers a handler for every HTTP method, which is what a reverse proxy forwarding arbitrary verbs needs. See MethodAny.

func (*Router) DELETE

func (r *Router) DELETE(pattern string, h http.Handler, opts ...RouteOption) error

DELETE registers a handler for DELETE requests.

func (*Router) Err added in v0.0.3

func (r *Router) Err() error

Err returns the first registration error recorded on the router, so that a caller which ignored individual return values can still check the table as a whole before serving:

api := r.Group("/api/v1", auth)
api.GET("/orders", listOrders)
api.POST("/orders", createOrder)
if err := r.Err(); err != nil {
    log.Fatalf("route table: %v", err)
}

It is the Router-level equivalent of Builder.Err.

func (*Router) Errs added in v0.0.3

func (r *Router) Errs() []error

Errs returns every registration error recorded on the router.

func (*Router) GET

func (r *Router) GET(pattern string, h http.Handler, opts ...RouteOption) error

GET registers a handler for GET requests.

func (*Router) Group

func (r *Router) Group(prefix string, mw ...Middleware) *Group

Group returns a group that prefixes its patterns and wraps its handlers.

func (*Router) HEAD

func (r *Router) HEAD(pattern string, h http.Handler, opts ...RouteOption) error

HEAD registers a handler for HEAD requests.

func (*Router) Handle

func (r *Router) Handle(method, pattern string, h http.Handler, opts ...RouteOption) error

Handle registers a handler for a method and pattern. It returns an error — never a panic — when the pattern is malformed, duplicates an existing route, or conflicts with one already registered.

func (*Router) HandleFunc

func (r *Router) HandleFunc(method, pattern string, h http.HandlerFunc, opts ...RouteOption) error

HandleFunc registers a handler function for a method and pattern.

func (*Router) Host

func (r *Router) Host(patterns ...string) *Group

Host returns a group whose routes only answer on the given host patterns. See ValidateHost for the accepted forms.

func (*Router) Lookup

func (r *Router) Lookup(req *http.Request) (*Route, bool)

Lookup returns the route that would serve req, without allocating and without capturing parameters.

func (*Router) LookupInto

func (r *Router) LookupInto(req *http.Request, params []PathParam) (*Route, []PathParam, bool)

LookupInto returns the route that would serve req and appends its captured parameters to params. Supplying a params slice with spare capacity makes the call allocation free.

func (*Router) Mount added in v0.0.3

func (r *Router) Mount(prefix string, h http.Handler, opts ...RouteOption) error

Mount hands every request at or below a prefix to one handler, for every HTTP method. It is how another router, a file server, a debug endpoint or any third-party http.Handler is attached to a path:

r.Mount("/debug/pprof", pprofHandler)
r.Mount("/static", http.FileServer(http.Dir("public")))
r.Mount("/legacy", oldRouter)

Both the prefix itself and its subtree are covered, so "/static" and "/static/css/app.css" both reach the handler.

The prefix is NOT stripped: the handler sees the request path as it arrived. A gateway proxying to a backend needs the full path, and a handler that wants the prefix removed can say so explicitly, which reads better than a routing rule that silently rewrites:

r.Mount("/static", http.StripPrefix("/static", fs))

Matching is on segment boundaries, so mounting "/api" does not capture "/apiary". A route registered under a mounted prefix still wins on specificity, which is what makes carving an exception out of a mount work:

r.Mount("/api", proxy)
r.GET("/api/health", localHealth)  // served locally, not proxied

See MountParam for the one pattern that conflicts with a mount.

func (*Router) OPTIONS

func (r *Router) OPTIONS(pattern string, h http.Handler, opts ...RouteOption) error

OPTIONS registers a handler for OPTIONS requests.

func (*Router) PATCH

func (r *Router) PATCH(pattern string, h http.Handler, opts ...RouteOption) error

PATCH registers a handler for PATCH requests.

func (*Router) POST

func (r *Router) POST(pattern string, h http.Handler, opts ...RouteOption) error

POST registers a handler for POST requests.

func (*Router) PUT

func (r *Router) PUT(pattern string, h http.Handler, opts ...RouteOption) error

PUT registers a handler for PUT requests.

func (*Router) Route

func (r *Router) Route(name string) *Route

Route returns the route registered under name, or nil.

func (*Router) Routes

func (r *Router) Routes() []RouteInfo

Routes returns a snapshot of every registered route, in registration order. It is everything a documentation generator needs: the template as written, the parameters it declares with their constraints and positions, the handler, and any annotations attached with WithMeta.

func (*Router) ServeHTTP

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP implements http.Handler.

func (*Router) String

func (r *Router) String() string

String renders the route table, which is useful in tests and in start-up logs.

func (*Router) Swap

func (r *Router) Swap(build func(*Builder) error) error

Swap replaces the route table. build is called with a fresh, empty builder; if it returns an error, or if any registration inside it fails, the router's existing table is left untouched. Otherwise the new table is installed atomically and requests already in flight finish against the old one.

This is the reload path: a route table assembled from user-supplied configuration can be rejected in full without taking the process down.

A Group obtained from the router before a swap belongs to the replaced builder and registering on it afterwards has no effect on the live table. Build every group from the Builder the callback is given.

func (*Router) Use

func (r *Router) Use(mw ...Middleware)

Use appends router-level middleware. It applies to every route, including routes registered before the call, outermost first in registration order.

Directories

Path Synopsis
internal
template
Package template parses route templates of the form "/users/{id}" and "/users/{id:[0-9]+}" into a compiled regular expression, an ordered list of variable names, and a reverse-building template used for URL construction.
Package template parses route templates of the form "/users/{id}" and "/users/{id:[0-9]+}" into a compiled regular expression, an ordered list of variable names, and a reverse-building template used for URL construction.
tree
Package tree implements the segment-indexed prefix tree used to match routes whose templates consist of static segments, whole-segment wildcards with an optional type constraint, and an optional trailing catch-all.
Package tree implements the segment-indexed prefix tree used to match routes whose templates consist of static segments, whole-segment wildcards with an optional type constraint, and an optional trailing catch-all.
Package muxcompat is a drop-in replacement for github.com/gorilla/mux.
Package muxcompat is a drop-in replacement for github.com/gorilla/mux.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL