-
Notifications
You must be signed in to change notification settings - Fork 0
Routers and Listeners
A router owns one listener, one base path and one route table. An
application has at least one — default — and may have any number more.
app := rex.New(rex.WithConfig(&rex.Config{
DefaultRouter: rex.RouterConfig{Addr: ":8080"},
}))
if err := app.CreateRouter("admin", rex.RouterConfig{Addr: ":9090"}); err != nil {
return err
}
_ = app.RegisterRoute(route.New("GET", "/api/users", listUsers)) // :8080
_ = app.RegisterRouteToRouter(route.New("GET", "/dashboard", dash), "admin") // :9090Blast radius and reachability. An admin panel on a separate port can be firewalled to a VPN while the API faces the internet. Health and metrics endpoints on their own listener stay answerable when the public listener is saturated — which is exactly when you need them.
Different limits. A public listener wants a 4 MiB body cap and a 30s read
timeout; a metrics endpoint wants 4 KiB and 5s. One RouterConfig per router
makes that expressible.
Different documents. The OpenAPI generator includes or excludes routes based on which router serves them, so an internal-only listener's routes stay out of the public specification.
Extensions create their own: rextension-health and rextension-metric each
put their endpoints on a dedicated router by default.
err := app.CreateRouter("metrics", rex.RouterConfig{
Addr: ":9091",
BaseURL: "/",
ReadTimeout: 5 * time.Second,
MaxBodyBytes: 4 << 10,
})
if err != nil && !errors.Is(err, rex.ErrRouterExists) {
return err
}Tolerate ErrRouterExists. Two extensions may both want a router called
metrics, and whichever runs second should reuse it rather than abort startup.
Note that the first creator's configuration wins — the second one's is
discarded.
A router may be created before Run or from an extension's OnInitialize /
OnStart. Routes may name a router that does not exist yet: the name is
resolved when the tables are built, and an unknown one is reported from Run
with the call site of the registration that named it.
Every router is declared and then built.
Declared — between creation and Freeze, routes and middleware are
collected into ordinary slices under a mutex. Nothing serves.
Built — Freeze composes each route's middleware chain, builds the trie,
and publishes the result by atomic swap. From then on ServeHTTP reads the
table without locking, and Register / Use return
ErrRouterFrozen.
The framework calls Freeze for you, in step 9 of Lifecycle, in sorted
router-name order so log output is reproducible.
Listeners bind synchronously, in sorted name order, in step 10 — after every
table is frozen. A bind failure is returned from Run, and every listener
already bound is stopped, so a partially-bound application does not keep serving
on some ports while reporting a startup failure.
if err := app.Run(); err != nil {
// "listen tcp :8080: bind: address already in use"
}Use Addr: "127.0.0.1:0" to bind a free port, then read the one actually bound:
addr := app.Routers()["default"].Addr()Routers() is empty before Run and returns the built routers by name after.
type Router interface {
LifecycleHook
Name() string
BaseURL() string // normalised: no trailing slash, "" for root
Register(rt route.Route) error
Routes() []route.Route
Use(mw Middleware, priority int) error
UsePerRoute(f PerRouteMiddleware, priority int) error
Freeze() error
Frozen() bool
ServeHTTP(w http.ResponseWriter, r *http.Request)
Start() error // binds, synchronously
Stop() error // graceful, idempotent
Addr() string // bound address, or the configured one
Subscribe(eventType string, handler func(event.Event))
PrintRootsTree()
}Application code rarely touches this — app.RegisterRoute and friends go
through it. It is here because Routers() hands them back, and because
PrintRootsTree() is a genuinely useful debugging call: it logs the route table
as a tree.
| Call | Applies to |
|---|---|
app.Use(mw) |
every router, current and future |
app.UseOnRouter(name, mw, priority) |
one named router |
app.UsePerRoute(f, priority) |
routes where the factory returns non-nil, on every router |
app.UsePerRouteOn(name, f, priority) |
the same, limited to one router |
app.UsePerRouter(f, priority) |
routers where the factory returns non-nil |
The router named need not exist yet. See Middleware.
Stop() on a router is graceful and idempotent. The framework stops every
listener before running OnStop hooks, so nothing new arrives while
extensions are tearing down — and a hook that stops a router again is harmless.
rex — Restful Extended eXperience · MIT · © 2026 Kryovyx · pre-1.0 (alpha), interfaces may change
Getting started
Core
Operating
Ecosystem