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
- Variables
- func NumParams(r *http.Request) int
- func Param(r *http.Request, name string) string
- func ParamAt(r *http.Request, i int) (name, value string, ok bool)
- func ParamMap(r *http.Request) map[string]string
- func SetParams(r *http.Request, params ...PathParam) *http.Request
- func ValidateHost(pattern string) error
- type Builder
- func (b *Builder) ANY(pattern string, h http.Handler, opts ...RouteOption) error
- func (b *Builder) DELETE(pattern string, h http.Handler, opts ...RouteOption) error
- func (b *Builder) Err() error
- func (b *Builder) Errs() []error
- func (b *Builder) GET(pattern string, h http.Handler, opts ...RouteOption) error
- func (b *Builder) Group(prefix string, mw ...Middleware) *Group
- func (b *Builder) HEAD(pattern string, h http.Handler, opts ...RouteOption) error
- func (b *Builder) Handle(method, pattern string, h http.Handler, opts ...RouteOption) error
- func (b *Builder) HandleFunc(method, pattern string, h http.HandlerFunc, opts ...RouteOption) error
- func (b *Builder) Host(patterns ...string) *Group
- func (b *Builder) Mount(prefix string, h http.Handler, opts ...RouteOption) error
- func (b *Builder) OPTIONS(pattern string, h http.Handler, opts ...RouteOption) error
- func (b *Builder) PATCH(pattern string, h http.Handler, opts ...RouteOption) error
- func (b *Builder) POST(pattern string, h http.Handler, opts ...RouteOption) error
- func (b *Builder) PUT(pattern string, h http.Handler, opts ...RouteOption) error
- func (b *Builder) Use(mw ...Middleware)
- type Group
- func (g *Group) ANY(pattern string, h http.Handler, opts ...RouteOption) error
- func (g *Group) DELETE(pattern string, h http.Handler, opts ...RouteOption) error
- func (g *Group) GET(pattern string, h http.Handler, opts ...RouteOption) error
- func (g *Group) Group(prefix string, mw ...Middleware) *Group
- func (g *Group) HEAD(pattern string, h http.Handler, opts ...RouteOption) error
- func (g *Group) Handle(method, pattern string, h http.Handler, opts ...RouteOption) error
- func (g *Group) HandleFunc(method, pattern string, h http.HandlerFunc, opts ...RouteOption) error
- func (g *Group) Host(patterns ...string) *Group
- func (g *Group) Hosts() []string
- func (g *Group) Mount(prefix string, h http.Handler, opts ...RouteOption) error
- func (g *Group) OPTIONS(pattern string, h http.Handler, opts ...RouteOption) error
- func (g *Group) PATCH(pattern string, h http.Handler, opts ...RouteOption) error
- func (g *Group) POST(pattern string, h http.Handler, opts ...RouteOption) error
- func (g *Group) PUT(pattern string, h http.Handler, opts ...RouteOption) error
- func (g *Group) Prefix() string
- func (g *Group) Use(mw ...Middleware)
- type Middleware
- type ParamInfo
- type PathParam
- type Route
- type RouteError
- type RouteInfo
- type RouteOption
- type Router
- func (r *Router) ANY(pattern string, h http.Handler, opts ...RouteOption) error
- func (r *Router) DELETE(pattern string, h http.Handler, opts ...RouteOption) error
- func (r *Router) Err() error
- func (r *Router) Errs() []error
- func (r *Router) GET(pattern string, h http.Handler, opts ...RouteOption) error
- func (r *Router) Group(prefix string, mw ...Middleware) *Group
- func (r *Router) HEAD(pattern string, h http.Handler, opts ...RouteOption) error
- func (r *Router) Handle(method, pattern string, h http.Handler, opts ...RouteOption) error
- func (r *Router) HandleFunc(method, pattern string, h http.HandlerFunc, opts ...RouteOption) error
- func (r *Router) Host(patterns ...string) *Group
- func (r *Router) Lookup(req *http.Request) (*Route, bool)
- func (r *Router) LookupInto(req *http.Request, params []PathParam) (*Route, []PathParam, bool)
- func (r *Router) Mount(prefix string, h http.Handler, opts ...RouteOption) error
- func (r *Router) OPTIONS(pattern string, h http.Handler, opts ...RouteOption) error
- func (r *Router) PATCH(pattern string, h http.Handler, opts ...RouteOption) error
- func (r *Router) POST(pattern string, h http.Handler, opts ...RouteOption) error
- func (r *Router) PUT(pattern string, h http.Handler, opts ...RouteOption) error
- func (r *Router) Route(name string) *Route
- func (r *Router) Routes() []RouteInfo
- func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)
- func (r *Router) String() string
- func (r *Router) Swap(build func(*Builder) error) error
- func (r *Router) Use(mw ...Middleware)
Constants ¶
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.
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.
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.
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 ¶
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 Param ¶
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 ¶
ParamAt returns the i'th path parameter in pattern order. It reports false when i is out of range.
func ParamMap ¶
ParamMap returns the captured parameters as a map. It allocates, and exists for callers that need the shape gorilla's Vars returned.
func SetParams ¶
SetParams attaches path parameters to a request. It is intended for tests that invoke a handler directly.
func ValidateHost ¶
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) Err ¶
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) Group ¶
func (b *Builder) Group(prefix string, mw ...Middleware) *Group
Group returns a group rooted at the builder.
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 ¶
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
Mount hands every request at or below a prefix to one handler. See Router.Mount.
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) 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) 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 ¶
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) Mount ¶ added in v0.0.3
Mount hands every request at or below a prefix, resolved against the group's own prefix, to one handler. See Router.Mount.
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 ¶
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.
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 ¶
RouteOf returns the route that matched the request, or nil. A static route is reported even though it attaches no parameters.
func (*Route) Handler ¶
Handler returns the handler the route was registered with, before middleware.
func (*Route) Hosts ¶
Hosts returns the host patterns the route is restricted to, as written. It returns nil when the route answers on any host.
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 (*Router) ANY ¶ added in v0.0.2
ANY registers a handler for every HTTP method, which is what a reverse proxy forwarding arbitrary verbs needs. See MethodAny.
func (*Router) Err ¶ added in v0.0.3
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) Group ¶
func (r *Router) Group(prefix string, mw ...Middleware) *Group
Group returns a group that prefixes its patterns and wraps its handlers.
func (*Router) Handle ¶
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 ¶
Host returns a group whose routes only answer on the given host patterns. See ValidateHost for the accepted forms.
func (*Router) Lookup ¶
Lookup returns the route that would serve req, without allocating and without capturing parameters.
func (*Router) LookupInto ¶
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
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) Routes ¶
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 ¶
String renders the route table, which is useful in tests and in start-up logs.
func (*Router) Swap ¶
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.
Source Files
¶
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. |